-
Notifications
You must be signed in to change notification settings - Fork 185
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
auto margins #1722
base: main
Are you sure you want to change the base?
auto margins #1722
Conversation
This feels almost ready; needs a bit of manual testing (and more snapshot tests to capture more ranges of ticks?). In particular with the auto mark. cc @tophtucker |
Can you regenerate the snapshots so we can more easily review the impact of this change? Thank you. 🙏 |
Here's a playground to develop tests: https://observablehq.com/@observablehq/auto-margins-1722 |
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 we’ll need to account for the tickFormat option when computing the automatic margins (even if it’s a custom function). It should be possible to compute the formatted tick strings, and then use their text metrics to more accurately infer the needed margins? Rather than assuming that numbers/dates/etc. are formatted a specific way?
Computing all the ticks is probably more adequate, but it is also quite a bit more complicated, since the ticks are computed from the instantiated scales. I've tried to approximate it by doing half-way measures (like, apply tickFormat if given… but on what ticks?) and it seemed worse. My conclusion was that either we have a light system (this one, to which we could add a few common cases such as type: "log"), or we go full-in and measure the actual text that will be rendered, by running a (fake) axisY mark with all the options? |
Yes, it’s complicated. 🤔 |
As discussed, we want to initialize (but not fully render) the axis text mark for y scales; this will give us the text channel which we can use to infer appropriate left & right margins. |
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.
If we support auto for marginLeft and marginRight, we should also support it for marginTop and marginBottom (even if those margins are comparatively “dumb” and just have the current behavior).
src/mark.js
Outdated
this.marginBottom = +marginBottom; | ||
this.marginLeft = +marginLeft; | ||
this.marginLeft = marginLeft === "auto" ? "auto" : +marginLeft; |
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.
Needs isAuto helper (for case-insensitivity).
I've implemented the new strategy. Still needs a bit of work:
|
I think this is good to go! Does it need documentation? |
src/dimensions.js
Outdated
// scales a second time. | ||
function autoMarginK(margin, {k: scale, labelAnchor, label}, options, mark, stateByMark, scales, dimensions, context) { | ||
const {data, facets} = stateByMark.get(mark); | ||
const {channels} = mark.initializer(data, facets, {}, scales, dimensions, context); |
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.
Don’t you want to call mark.initialize here, not mark.initializer? The latter only applies to marks with an initializer, but here we want to initialize the mark (without assuming that it has an initializer).
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 we need the initializer because the actual ticks depend on the instantiated scales. We know that the mark has an initializer because it can only be a (tick label) axis mark.
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.
My point is that we should follow the same code path that Plot.plot follows:
- call mark.initialize first
- if mark.initializer exists, call that second
We shouldn’t make such a strong assumption about how the mark is implemented. Also, I wasn’t expecting that the mark would already be initialized (using stateByMark); I was thinking we would initialize it ourselves here. So that further complicates matters.
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.
When we call autoMarginK all the marks have been initialized already, which is why we can avoid the cost of re-initializing them. I can see how reaching for stateByMark can feel messy, though. 🥺
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.
If the mark is already initialized here, why do we need to invoke its initializer again?
src/marks/axis.js
Outdated
@@ -81,9 +81,9 @@ function axisKy( | |||
x, | |||
margin, | |||
marginTop = margin === undefined ? 20 : margin, | |||
marginRight = margin === undefined ? (anchor === "right" ? 40 : 0) : margin, | |||
marginRight = margin === undefined ? (anchor === "right" ? (x == null ? k : 40) : 0) : margin, |
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.
Temporarily setting this to k
feels a bit dirty… it’s effectively a hidden API if you set the marginRight option to "x" or "y", right?
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.
Agree, but I didn't find a better way.
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.
You’ll likely have to change how the defaults are initialized rather than using default initializers.
src/plot.js
Outdated
const {scaleDescriptors, scales, dimensions, subdimensions, superdimensions} = createDimensionsScales( | ||
channels, | ||
marks, | ||
stateByMark, | ||
options, | ||
context | ||
); |
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.
We might want to re-inline the createDimensionsScales function into plot here, given the number of arguments and return values — not sure it’s beneficial to break it out into a separate function. Perhaps I should investigate and see how it feels.
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.
The thing is we need to run it twice, so inlining it means we have to repeat these lines. Also it felt better to have the part about autoMargins live in dimensions.js (although I'd prefer to have the scales part live in plot.js… and they're intertwined!
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've inlined and minimized it. Hope this is more readable
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 looks great overall. I think I want to fiddle with the implementation a bit, and in particular I want to use mark.initialize instead of mark.initializer, but the results seem good and the implementation manageable. Do you feel good about it?
Yes I feel good about the result: the tests are all looking great and the only changes are improvements. I think the algorithm is correct. The implementation can certainly be better. |
One thing that bugged me is that monospaceWidth and defaultWidth are not aligned; maybe there's still time to change this by making monospace chars 60 instead of 100 — it would be a slightly breaking change, but it would make the two lengths comparable, which is something that was needed here. |
…of the actual tick labels is too large.
fix a bug with inferred labels, which can be empty ("Date", "Year"… are filtered out). observe marginLeft and marginRight when specified in the axis mark (this wasn't tested and had lead to a regression)
(tested by http://localhost:8008/?test=marginsMonospace)
Alternative to #1714. The axis mark sets the value of marginLeft (etc.) not to a number, but to a special value ("auto", not a symbol if we want to keep this usable from outside); the actual numeric value of that special value is computed in createDimensions—where the scales’ types and domains are already materialized
TODO:
closes #1451
closes #383
closes #1859