Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
- Notifications
You must be signed in to change notification settings - Fork 2k
Fix axis line width, length, and positioning for coupled subplots#1854
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
Uh oh!
There was an error while loading. Please reload this page.
Changes from all commits
829cd6efe1615b5b495e6543d6b0eb48a9daaba53027d2ca26d41a37b31e60d274526a16bae9c893588d739c998File filter
Filter by extension
Conversations
Uh oh!
There was an error while loading. Please reload this page.
Jump to
Diff view
Diff view
Uh oh!
There was an error while loading. Please reload this page.
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -42,10 +42,16 @@ function overlappingDomain(xDomain, yDomain, domains) { | ||
| } | ||
| exports.lsInner = function(gd) { | ||
| var fullLayout = gd._fullLayout, | ||
| gs = fullLayout._size, | ||
| axList = Plotly.Axes.list(gd), | ||
| i; | ||
| var fullLayout = gd._fullLayout; | ||
| var gs = fullLayout._size; | ||
| var pad = gs.p; | ||
| var axList = Plotly.Axes.list(gd); | ||
| // _has('cartesian') means SVG specifically, not GL2D - but GL2D | ||
| // can still get here because it makes some of the SVG structure | ||
| // for shared features like selections. | ||
| var hasSVGCartesian = fullLayout._has('cartesian'); | ||
| var i; | ||
| // clear axis line positions, to be set in the subplot loop below | ||
| for(i = 0; i < axList.length; i++) axList[i]._linepositions = {}; | ||
| @@ -80,11 +86,9 @@ exports.lsInner = function(gd) { | ||
| return; | ||
| } | ||
| var xa = Plotly.Axes.getFromId(gd, subplot, 'x'), | ||
| ya = Plotly.Axes.getFromId(gd, subplot, 'y'), | ||
| xDomain = xa.domain, | ||
| yDomain = ya.domain, | ||
| plotgroupBgData = []; | ||
| var xDomain = plotinfo.xaxis.domain; | ||
| var yDomain = plotinfo.yaxis.domain; | ||
| var plotgroupBgData = []; | ||
| if(overlappingDomain(xDomain, yDomain, lowerDomains)) { | ||
| plotgroupBgData = [0]; | ||
| @@ -125,22 +129,21 @@ exports.lsInner = function(gd) { | ||
| fullLayout._plots[subplot].bg = d3.select(this); | ||
| }); | ||
| var freefinished = []; | ||
| var freeFinished = {}; | ||
| subplotSelection.each(function(subplot) { | ||
| var plotinfo = fullLayout._plots[subplot]; | ||
| var xa = Plotly.Axes.getFromId(gd, subplot, 'x'), | ||
| ya = Plotly.Axes.getFromId(gd, subplot, 'y'); | ||
| var xa = plotinfo.xaxis; | ||
| var ya = plotinfo.yaxis; | ||
| // reset scale in case the margins have changed | ||
| xa.setScale(); | ||
| ya.setScale(); | ||
| if(plotinfo.bg && fullLayout._has('cartesian')) { | ||
| if(plotinfo.bg && hasSVGCartesian) { | ||
| plotinfo.bg | ||
| .call(Drawing.setRect, | ||
| xa._offset - gs.p, ya._offset - gs.p, | ||
| xa._length + 2 * gs.p, ya._length + 2 * gs.p) | ||
| xa._offset - pad, ya._offset - pad, | ||
| xa._length + 2 * pad, ya._length + 2 * pad) | ||
| .call(Color.fill, fullLayout.plot_bgcolor) | ||
| .style('stroke-width', 0); | ||
| } | ||
| @@ -165,7 +168,7 @@ exports.lsInner = function(gd) { | ||
| 'height': ya._length | ||
| }); | ||
| plotinfo.plot.call(Drawing.setTranslate, xa._offset, ya._offset); | ||
| Drawing.setTranslate(plotinfo.plot, xa._offset, ya._offset); | ||
| var plotClipId; | ||
| var layerClipId; | ||
| @@ -191,126 +194,153 @@ exports.lsInner = function(gd) { | ||
| // to DRY up Drawing.setClipUrl calls downstream | ||
| plotinfo.layerClipId = layerClipId; | ||
| var xlw = Drawing.crispRound(gd, xa.linewidth, 1), | ||
| ylw = Drawing.crispRound(gd, ya.linewidth, 1), | ||
| xp = gs.p + ylw, | ||
| xpathPrefix = 'M' + (-xp) + ',', | ||
| xpathSuffix = 'h' + (xa._length + 2 * xp), | ||
| showfreex = xa.anchor === 'free' && | ||
| freefinished.indexOf(xa._id) === -1, | ||
| freeposx = gs.h * (1 - (xa.position||0)) + ((xlw / 2) % 1), | ||
| showbottom = | ||
| (xa.anchor === ya._id && (xa.mirror || xa.side !== 'top')) || | ||
| xa.mirror === 'all' || xa.mirror === 'allticks' || | ||
| (xa.mirrors && xa.mirrors[ya._id + 'bottom']), | ||
| bottompos = ya._length + gs.p + xlw / 2, | ||
| showtop = | ||
| (xa.anchor === ya._id && (xa.mirror || xa.side === 'top')) || | ||
| xa.mirror === 'all' || xa.mirror === 'allticks' || | ||
| (xa.mirrors && xa.mirrors[ya._id + 'top']), | ||
| toppos = -gs.p - xlw / 2, | ||
| // shorten y axis lines so they don't overlap x axis lines | ||
| yp = gs.p, | ||
| // except where there's no x line | ||
| // TODO: this gets more complicated with multiple x and y axes | ||
| ypbottom = showbottom ? 0 : xlw, | ||
| yptop = showtop ? 0 : xlw, | ||
| ypathSuffix = ',' + (-yp - yptop) + | ||
| 'v' + (ya._length + 2 * yp + yptop + ypbottom), | ||
| showfreey = ya.anchor === 'free' && | ||
| freefinished.indexOf(ya._id) === -1, | ||
| freeposy = gs.w * (ya.position||0) + ((ylw / 2) % 1), | ||
| showleft = | ||
| (ya.anchor === xa._id && (ya.mirror || ya.side !== 'right')) || | ||
| ya.mirror === 'all' || ya.mirror === 'allticks' || | ||
| (ya.mirrors && ya.mirrors[xa._id + 'left']), | ||
| leftpos = -gs.p - ylw / 2, | ||
| showright = | ||
| (ya.anchor === xa._id && (ya.mirror || ya.side === 'right')) || | ||
| ya.mirror === 'all' || ya.mirror === 'allticks' || | ||
| (ya.mirrors && ya.mirrors[xa._id + 'right']), | ||
| rightpos = xa._length + gs.p + ylw / 2; | ||
| var xIsFree = !xa._anchorAxis; | ||
| var showFreeX = xIsFree && !freeFinished[xa._id]; | ||
| var showBottom = shouldShowLine(xa, ya, 'bottom'); | ||
| var showTop = shouldShowLine(xa, ya, 'top'); | ||
| var yIsFree = !ya._anchorAxis; | ||
| var showFreeY = yIsFree && !freeFinished[ya._id]; | ||
| var showLeft = shouldShowLine(ya, xa, 'left'); | ||
| var showRight = shouldShowLine(ya, xa, 'right'); | ||
| var xlw = Drawing.crispRound(gd, xa.linewidth, 1); | ||
| var ylw = Drawing.crispRound(gd, ya.linewidth, 1); | ||
| /* | ||
| * x lines get longer where they meet y lines, to make a crisp corner. | ||
| * The x lines get the padding (margin.pad) plus the y line width to | ||
| * fill up the corner nicely. Free x lines are excluded - they always | ||
| * span exactly the data area of the plot | ||
| * | ||
| * | XXXXX | ||
| * | XXXXX | ||
| * | | ||
| * +------ | ||
| * x1 | ||
| * ----- | ||
| * x2 | ||
| */ | ||
| var leftYLineWidth = findCounterAxisLineWidth(gd, xa, ylw, showLeft, 'left', axList); | ||
| var xLinesXLeft = (!xIsFree && leftYLineWidth) ? | ||
| (-pad - leftYLineWidth) : 0; | ||
| var rightYLineWidth = findCounterAxisLineWidth(gd, xa, ylw, showRight, 'right', axList); | ||
| var xLinesXRight = xa._length + ((!xIsFree && rightYLineWidth) ? | ||
| (pad + rightYLineWidth) : 0); | ||
| var xLinesYFree = gs.h * (1 - (xa.position || 0)) + ((xlw / 2) % 1); | ||
| var xLinesYBottom = ya._length + pad + xlw / 2; | ||
| var xLinesYTop = -pad - xlw / 2; | ||
| /* | ||
| * y lines that meet x axes get longer only by margin.pad, because | ||
| * the x axes fill in the corner space. Free y axes, like free x axes, | ||
| * always span exactly the data area of the plot | ||
| * | ||
| * | | XXXX | ||
| * y2| y1| XXXX | ||
| * | | XXXX | ||
| * | | ||
| * +----- | ||
| */ | ||
| var connectYBottom = !yIsFree && findCounterAxisLineWidth( | ||
| gd, ya, xlw, showBottom, 'bottom', axList); | ||
| var yLinesYBottom = ya._length + (connectYBottom ? pad : 0); | ||
| var connectYTop = !yIsFree && findCounterAxisLineWidth( | ||
| gd, ya, xlw, showTop, 'top', axList); | ||
| var yLinesYTop = connectYTop ? -pad : 0; | ||
| var yLinesXFree = gs.w * (ya.position || 0) + ((ylw / 2) % 1); | ||
| var yLinesXLeft = -pad - ylw / 2; | ||
| var yLinesXRight = xa._length + pad + ylw / 2; | ||
| function xLinePath(y, showThis) { | ||
| if(!showThis) return ''; | ||
| return 'M' + xLinesXLeft + ',' + y + 'H' + xLinesXRight; | ||
| } | ||
| function yLinePath(x, showThis) { | ||
| if(!showThis) return ''; | ||
| return 'M' + x + ',' + yLinesYTop + 'V' + yLinesYBottom; | ||
| } | ||
| // save axis line positions for ticks, draggers, etc to reference | ||
| // each subplot gets an entry: | ||
| // [left or bottom, right or top, free, main] | ||
| // main is the position at which to draw labels and draggers, if any | ||
| xa._linepositions[subplot] = [ | ||
| showbottom ? bottompos : undefined, | ||
| showtop ? toppos : undefined, | ||
| showfreex ? freeposx : undefined | ||
| showBottom ? xLinesYBottom : undefined, | ||
| showTop ? xLinesYTop : undefined, | ||
| showFreeX ? xLinesYFree : undefined | ||
| ]; | ||
| if(xa.anchor === ya._id) { | ||
| if(xa._anchorAxis === ya) { | ||
| xa._linepositions[subplot][3] = xa.side === 'top' ? | ||
| toppos : bottompos; | ||
| xLinesYTop : xLinesYBottom; | ||
| } | ||
| else if(showfreex) { | ||
| xa._linepositions[subplot][3] = freeposx; | ||
| else if(showFreeX) { | ||
| xa._linepositions[subplot][3] = xLinesYFree; | ||
| } | ||
| ya._linepositions[subplot] = [ | ||
| showleft ? leftpos : undefined, | ||
| showright ? rightpos : undefined, | ||
| showfreey ? freeposy : undefined | ||
| showLeft ? yLinesXLeft : undefined, | ||
| showRight ? yLinesXRight : undefined, | ||
| showFreeY ? yLinesXFree : undefined | ||
| ]; | ||
| if(ya.anchor === xa._id) { | ||
| if(ya._anchorAxis === xa) { | ||
| ya._linepositions[subplot][3] = ya.side === 'right' ? | ||
| rightpos : leftpos; | ||
| yLinesXRight : yLinesXLeft; | ||
| } | ||
| else if(showfreey) { | ||
| ya._linepositions[subplot][3] = freeposy; | ||
| else if(showFreeY) { | ||
| ya._linepositions[subplot][3] = yLinesXFree; | ||
| } | ||
| // translate all the extra stuff to have the | ||
| // same origin as the plot area or axes | ||
| var origin = 'translate(' + xa._offset + ',' + ya._offset + ')', | ||
| originx = origin, | ||
| originy = origin; | ||
| if(showfreex) { | ||
| originx = 'translate(' + xa._offset + ',' + gs.t + ')'; | ||
| toppos += ya._offset - gs.t; | ||
| bottompos += ya._offset - gs.t; | ||
| var origin = 'translate(' + xa._offset + ',' + ya._offset + ')'; | ||
| var originX = origin; | ||
| var originY = origin; | ||
| if(showFreeX) { | ||
| originX = 'translate(' + xa._offset + ',' + gs.t + ')'; | ||
| xLinesYTop += ya._offset - gs.t; | ||
| xLinesYBottom += ya._offset - gs.t; | ||
| } | ||
| if(showfreey) { | ||
| originy = 'translate(' + gs.l + ',' + ya._offset + ')'; | ||
| leftpos += xa._offset - gs.l; | ||
| rightpos += xa._offset - gs.l; | ||
| if(showFreeY) { | ||
| originY = 'translate(' + gs.l + ',' + ya._offset + ')'; | ||
| yLinesXLeft += xa._offset - gs.l; | ||
| yLinesXRight += xa._offset - gs.l; | ||
| } | ||
| if(fullLayout._has('cartesian')) { | ||
| if(hasSVGCartesian) { | ||
| plotinfo.xlines | ||
| .attr('transform', originx) | ||
| .attr('transform', originX) | ||
| .attr('d', ( | ||
| (showbottom ? (xpathPrefix + bottompos + xpathSuffix) : '') + | ||
| (showtop ? (xpathPrefix + toppos + xpathSuffix) : '') + | ||
| (showfreex ? (xpathPrefix + freeposx + xpathSuffix) : '')) || | ||
| xLinePath(xLinesYBottom, showBottom) + | ||
| xLinePath(xLinesYTop, showTop) + | ||
| xLinePath(xLinesYFree, showFreeX)) || | ||
| // so it doesn't barf with no lines shown | ||
| 'M0,0') | ||
| .style('stroke-width', xlw + 'px') | ||
| .call(Color.stroke, xa.showline ? | ||
| xa.linecolor : 'rgba(0,0,0,0)'); | ||
| plotinfo.ylines | ||
| .attr('transform', originy) | ||
| .attr('transform', originY) | ||
| .attr('d', ( | ||
| (showleft ? ('M' + leftpos + ypathSuffix) : '') + | ||
| (showright ? ('M' + rightpos + ypathSuffix) : '') + | ||
| (showfreey ? ('M' + freeposy + ypathSuffix) : '')) || | ||
| yLinePath(yLinesXLeft, showLeft) + | ||
| yLinePath(yLinesXRight, showRight) + | ||
| yLinePath(yLinesXFree, showFreeY)) || | ||
| 'M0,0') | ||
| .attr('stroke-width', ylw + 'px') | ||
| .style('stroke-width', ylw + 'px') | ||
| .call(Color.stroke, ya.showline ? | ||
| ya.linecolor : 'rgba(0,0,0,0)'); | ||
| } | ||
| plotinfo.xaxislayer.attr('transform', originx); | ||
| plotinfo.yaxislayer.attr('transform', originy); | ||
| plotinfo.xaxislayer.attr('transform', originX); | ||
| plotinfo.yaxislayer.attr('transform', originY); | ||
| plotinfo.gridlayer.attr('transform', origin); | ||
| plotinfo.zerolinelayer.attr('transform', origin); | ||
| plotinfo.draglayer.attr('transform', origin); | ||
| // mark free axes as displayed, so we don't draw them again | ||
| if(showfreex) { freefinished.push(xa._id); } | ||
| if(showfreey) { freefinished.push(ya._id); } | ||
| if(showFreeX) freeFinished[xa._id] = 1; | ||
| if(showFreeY) freeFinished[ya._id] = 1; | ||
Contributor There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Smart. I usually set keys like this as | ||
| }); | ||
| Plotly.Axes.makeClipPaths(gd); | ||
| @@ -320,6 +350,65 @@ exports.lsInner = function(gd) { | ||
| return gd._promises.length && Promise.all(gd._promises); | ||
| }; | ||
| function shouldShowLine(ax, counterAx, side) { | ||
| return (ax._anchorAxis === counterAx && (ax.mirror || ax.side === side)) || | ||
| ax.mirror === 'all' || ax.mirror === 'allticks' || | ||
| (ax.mirrors && ax.mirrors[counterAx._id + side]); | ||
| } | ||
| function findCounterAxes(gd, ax, axList) { | ||
| var counterAxes = []; | ||
| var anchorAx = ax._anchorAxis; | ||
| if(anchorAx) { | ||
| var counterMain = anchorAx._mainAxis; | ||
| if(counterAxes.indexOf(counterMain) === -1) { | ||
| counterAxes.push(counterMain); | ||
| for(var i = 0; i < axList.length; i++) { | ||
| if(axList[i].overlaying === counterMain._id && | ||
| counterAxes.indexOf(axList[i]) === -1 | ||
| ) { | ||
| counterAxes.push(axList[i]); | ||
| } | ||
| } | ||
| } | ||
| } | ||
| return counterAxes; | ||
| } | ||
| function findLineWidth(gd, axes, side) { | ||
| for(var i = 0; i < axes.length; i++) { | ||
| var ax = axes[i]; | ||
| var anchorAx = ax._anchorAxis; | ||
| if(anchorAx && shouldShowLine(ax, anchorAx, side)) { | ||
| return Drawing.crispRound(gd, ax.linewidth); | ||
| } | ||
| } | ||
| } | ||
| function findCounterAxisLineWidth(gd, ax, subplotCounterLineWidth, | ||
| subplotCounterIsShown, side, axList) { | ||
| if(subplotCounterIsShown) return subplotCounterLineWidth; | ||
| var i; | ||
| // find all counteraxes for this one, then of these, find the | ||
| // first one that has a visible line on this side | ||
| var mainAxis = ax._mainAxis; | ||
| var counterAxes = findCounterAxes(gd, mainAxis, axList); | ||
| var lineWidth = findLineWidth(gd, counterAxes, side); | ||
| if(lineWidth) return lineWidth; | ||
| for(i = 0; i < axList.length; i++) { | ||
| if(axList[i].overlaying === mainAxis._id) { | ||
| counterAxes = findCounterAxes(gd, axList[i], axList); | ||
| lineWidth = findLineWidth(gd, counterAxes, side); | ||
| if(lineWidth) return lineWidth; | ||
| } | ||
| } | ||
| return 0; | ||
| } | ||
| exports.drawMainTitle = function(gd) { | ||
| var fullLayout = gd._fullLayout; | ||
Uh oh!
There was an error while loading. Please reload this page.
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.
Good catch. Is
.attr('stroke-width', /* */)a thing?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.
Yes, a lot of properties in SVG you can use either
.attror.style, with slightly different rules but also very different precedence - see https://css-tricks.com/presentation-attributes-vs-inline-styles/ - attributes are about the lowest, while inline styles are nearly the highest.@monfera and I had a discussion about this a few days ago, I think for our purposes
.styleshould be preferred.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.
Ha, interesting. It's something we might to standardize in
strict-d3.js.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.
I think
stroke-widthas an attribute doesn't need the'px'but also, the question kept brewing in my mind and I like the (user-experienced) flexibility of using attributes where possible, as it's easy to restyle them. WithD3the typical usage is inline styling of the element, which is for our current context, the highest priority, ie. the user would have a hard time overriding it (there are two main ways: 1) user reselects with D3 and changes the inline style; 2) user specifies!importantstyling in the style sheet. So, usingattrwould be more convenient for users if they choose to restyle via CSS.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.
... also, there's the thing that Microsoft browsers (IE, Edge) don't have full CSS support, so if the decision is to go with
styleas much as we can, then we still must useattrfortransformand possibly some more presentation attributes, ie. somestyle->attrswitch may need to happen during MS testingThere 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.
Right,
transformshould stay as anattrfor MS support. I think even some things likexandythat we've always provided as attributes can be styles. I think my own proclivity is to treat positioning as attributes and everything else as style, but I'm sure I've broken that rule. Might be worthwhile to create a list of property names we use and whether each should be used as a style or attribute? And then yes, we could enforce that instrict-d3.I'm sympathetic to the idea of giving users more control, but in the end I think it would be a mistake to encourage overriding internals of the plot with CSS - we should push people to use the plot JSON instead - for three reasons:
.point,.xtitle, that could get mangled easily if the page does something else with those classes.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.
Yes these points are all true. We don't currently go out of our way to dissuade scenegraph class based styling - we don't generate mangled or one-off class names or otherwise actively dissuade external styling. Also on the forum, SO and gh questions sometimes our answer is that a particular wish isn't supported but can be done with a
d3.selectAll('whatever').style(...)and our scenegraph class is human friendly so I over-inferred 😄 That and the upside of not having to have heuristics for when to switch toattr. Btw. as a developer either way is fine as long as there's a guideline like what you mentioned above @alexcjohnsonNot sure if the emerging linter solution already covers it, I think this is the list of attributes (ie. all presentation attributes) that can go either way.
Unfortunately it's quite Venn-diagrammey and common stuff like
x,dx,retc. ie. drivers of the most basic geometry are not on it so even in the future, those things won't be able to use CSS transitions/animations. (Maybe interesting is that the SMIL deprecation note got retracted.)