-
Notifications
You must be signed in to change notification settings - Fork 294
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
One #300
One #300
Conversation
Thoughts? If I add tests and docs would there be interest? |
I agree that it could be a useful shorthand notation, but I'm afraid it introduces some sort of conceptual problems. One can read the proposal as a way of extending document.querySelector from a "query" to a "query or create". But in a sense it's very limiting to accept only "nodeName.className" selectors, and users will want to extend it to more selectors (such as nodeName#id, nodeName.class1.class2, etc). Of course, it isn't possible to extend to arbitrary selectors (if we don't know the nodeName, we can't invent it, and supporting :nth-of-type or other subtle selectors is going to require a lot of magic tricks). In the same vein, if this PR was accepted it would be consistent to support append("nodeName.className") and join("nodeName.className"), and maybe also with #id. I often use the following pattern instead: |
Thank you @Fil for giving this some thought. I understand what you are saying regarding the conceptual problems it introduces.
I am of the opinion that it should be limiting, with its scope only supporting a single class. If folks want to use it and assign multiple classes, the existing API should be used.
That is interesting and I had not considered touching other parts of the API. But yes it might make sense. Assigning classes for the purpose of making specific D3 selections may well be one of the most common uses of
To summarize the world in which this addition might make sense:
While those changes would compliment |
I don’t want to introduce (and design and maintain) a new string-based DSL. D3 currently doesn’t need to understand how to parse a selector—it can just pass it through to element.querySelector or element.querySelectorAll. And likewise it can pass a tagName to document.createElement. If you want this sort of shorthand I suggest using d3-jetpack. |
Got it. Thanks! |
I have simplified the implementation to avoid a DSL: const one = (selection, name, className) =>
selection
.selectAll(name + '.' + className)
.data([null])
.join(name)
.attr('class', className); |
I'd most definitely use this, I think |
Thanks! I find myself using this all the time for managing |
Most common use cases:
|
src/one.js
Outdated
@@ -0,0 +1,6 @@ | |||
export default (selection, name, className) => | |||
selection | |||
.selectAll(`${name}.${className}`) |
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.
Looking at this again, className
should probably be made optional. It's nice sometimes to omit a class name if it's not necessary.
Made some updates here:
I've been using this utility in my work all the time and it works out great in that it reduces the amount of code significantly. Is there anything else I should add here? Thanks! |
Be great to get this in, I'd like to start using it immediately on some projects. |
Example uses: SVG Containerconst svg = one(select(container), 'svg')
.attr('width', width)
.attr('height', height); Axesone(selection, 'g', 'x-axis')
.attr('transform', `translate(0, ${height - bottom})`)
.call(axisBottom(xScale));
one(selection, 'g', 'y-axis')
.attr('transform', `translate(${left}, 0)`)
.call(axisLeft(yScale)); Axis Labelsconst g = one(selection, 'g', 'axis-labels')
.attr('text-anchor', 'middle')
.attr('font-size', '12px')
.attr('font-family', 'sans-serif');
one(g, 'text', 'x-axis-label')
.attr('x', left + (width - right - left) / 2)
.attr('y', height - bottom + xLabelOffset)
.attr('alignment-baseline', 'hanging')
.text(xLabel);
one(g, 'text', 'y-axis-label')
.attr('x', -(top + (height - bottom - top) / 2))
.attr('y', left - yLabelOffset)
.attr('transform', 'rotate(-90)')
.text(yLabel); Marks Containerone(selection, 'g', 'marks')
.selectAll('circle')
.data(data)
.join('circle')
.attr('cx', (d) => xScale(xValue(d)))
.attr('cy', (d) => yScale(yValue(d)))
.attr('r', circleRadius); |
Still copying this implementation into fresh projects all the time! Anyone else using this? |
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 can see the appeal, but I don't think this works. It feels both too specific and not strict enough.
It's usable only for ("element") or ("element", "class"); it cannot, for example, be used to add a unique element with a given aria-label, or id. And I don't think there is a way to make it more generic without taking care of the selector dsl, which we don't want to do anywhere in d3-selection.
It's not chainable (like .join is), so you have to write d3.one(d3.select(chart), "g", marks)
, not d3.select(chart).one("g", marks)
.
The implementation uses .attr("class", className)
, this will erase any class name that was already on the previous element (if there was one). In other words, updating that element will reset a class that we added to it. This might be desired, or not, depending on the application, but the API gives no clue that this is what is going to happen, and no way to change it in one direction or the other.
Instead, a user could do:
selection.selectAll("g").data([1]).join("g"); // element
selection.selectAll("g.marks").data([1]).join("g").attr("class", "marks"); // element, (re)sets class
selection.selectAll("g.marks").data([1]).join("g").classed("marks", true); // element, adds className
selection.selectAll(`#${id}`).data([1]).join("g").attr("id", id); // element, id
I can see how that is a bit tedious, because we specify things twice (element.className
, element
, className
) instead of (element
, className
); but it's explicit about what it does, does not refer to a DSL, and is easier to adapt.
TBH I'm not a fan of .data(data).join("g")
in the examples above. I often would want to use .join("g", data)
instead—but it's for a separate discussion.
Thank you @Fil for your thoughtful reply! Zooming out a bit, the core problem I'd like to see solved, somehow, is a concise way to write idempotent rendering logic that manages a single element. FWIW, this pattern is something I learned from studying the d3-axis implementation, which uses The only reason I introduced a class is to handle the case where you want to manage a single element that's the same type as a sibling, and the class is just to disambiguate between them. You raise a great point that an id could also be used to do the same thing. You also raise a great point that this approach is not conducive to having multiple classes on the element because it clobbers any existing classes. Regarging chaining, maybe that's a better API, the I suppose I'm really just looking for the "best practice" of how to manage a single DOM element with D3. This may be the current best solution:
It's just disappointing to me how verbose this is, because I find myself writing this same logic over and over again in projects and it really adds up. For the use cases I've encountered, I have not ever actually needed to put multiple classes on elements managed in this way, so the class clobbering issue is not something I've faced in practice. Anyway, thanks for looking at it, I appreciate the effort! It looks like this PR will never get merged because the pattern is fundamentally flawed. I'd like to keep searching for the right solution though, the "best practice" for managing a singular element with D3. |
Here's another example of code that could be made less verbose: import { axisLeft, axisBottom } from 'd3';
export const axes = (
selection,
{ xScale, yScale }
) => {
selection
.selectAll('g.y-axis')
.data([null])
.join('g')
.attr('class', 'y-axis')
.attr(
'transform',
`translate(${xScale.range()[0]},0)`
)
.call(axisLeft(yScale));
selection
.selectAll('g.x-axis')
.data([null])
.join('g')
.attr('class', 'x-axis')
.attr(
'transform',
`translate(0,${yScale.range()[0]})`
)
.call(axisBottom(xScale));
}; Any suggestions for making this more concise using the existing D3 APIs? |
I called ChatGPT for help refactoring: const createAxis = (sel, sc, ax, t) => sel
.selectAll(`g.${ax}`).data([null]).join('g')
.attr('class', ax).attr('transform', `translate(${t})`)
.call(ax(sc));
export const axes = (sel, { x, y }) => {
createAxis(sel, y, axisLeft, [x.range()[0], 0]);
createAxis(sel, x, axisBottom, [0, y.range()[0]]);
}; Note that ChatGPT is slightly wrong, but you get the idea. I think we can stop here, maybe continue the discussion on slack instead. |
Indeed. Thanks! |
Closes #294