d3 / d3-selection

Transform the DOM by selecting elements and joining to data.
https://d3js.org/d3-selection
ISC License
558 stars 294 forks source link

Selection.one #294

Closed curran closed 1 year ago

curran commented 2 years ago

Managing a single element is a common scenario. For example, a group element that contains an axis or legend. It might be a nice idea to consider introducing a utility that makes it simpler to manage a single element.

The most common pattern I find myself using is something like this:

const xAxisG = g.selectAll('.x-axis').data([null]).join('g').attr('class, 'x-axis');

Something similar can be seen in d3-axis for managing the domain path.

Wouldn't it be nicer to have a utility that does this:

const xAxisG = d3.one(g, 'g', 'x-axis');

?

Here's a strawman implementation:

const one = (selection, tagName, className) =>
  selection.selectAll('.' + className).data([null]).join(tagName).attr('class', className)
curran commented 2 years ago

An iteration:

    // Manages a single element.
    const one = (selection, className, name = 'g') =>
      selection
        .selectAll(name + '.' + className)
        .data([null])
        .join(name)
        .attr('class', className);

Example snippet before:

      svg
        .selectAll('.y-axis')
        .data([null])
        .join('g')
        .attr('class', 'y-axis')
        .attr('transform', `translate(${margin.left}, 0)`)
        .call(axisLeft(yScale));

After:

      one(svg, 'y-axis')
        .attr('transform', `translate(${margin.left}, 0)`)
        .call(axisLeft(yScale));
curran commented 2 years ago

It could even parse the tag name and class from a selector string:

const one = (selection, selector) => {
  const [name, className] = selector.split('.');
  return selection
    .selectAll(name + '.' + className)
    .data([null])
    .join(name)
    .attr('class', className);
}

I like this because when you read its invocation, the tag name is clear, and there is no "magic" (no default "g" value).

one(svg, 'g.y-axis')
  .attr('transform', `translate(${margin.left}, 0)`)
  .call(axisLeft(yScale));
curran commented 2 years ago

Would be super useful right now.

I have this in some code currently:

svg.append("g").call(xAxis);

and I need to change it to this:

svg
  .selectAll('.x-axis')
  .data([null])
  .join('g')
  .attr('class', 'x-axis')
  .call(xAxis)

but I wish I could change it to this instead:

one(svg, 'g.x-axis').call(xAxis);

Would be super nice!

curran commented 2 years ago

Another use case - setting up the SVG container:

Before:

const svg = select('body')
  .selectAll('.viz')
  .data([null])
  .join('svg')
  .attr('class', 'viz')
  .attr('width', width)
  .attr('height', height);

After:

const svg = one(select('body'), 'svg.viz')
  .attr('width', width)
  .attr('height', height);
curran commented 2 years ago

Closing as the change is not welcome https://github.com/d3/d3-selection/pull/300#issuecomment-1060000113

curran commented 2 years ago

Actually, wait a minute - we can avoid a string-based DSL and still implement the module.

Based on this feedback from https://github.com/d3/d3-selection/pull/300#issuecomment-1060000113:

I don’t want to introduce (and design and maintain) a new string-based DSL.

the proposal can be modified back to its original form:

one(g, 'g', 'x-axis');
curran commented 2 years ago

The class name argument could be optional. For example:

one(svg, 'g');

Example from https://github.com/d3/d3-selection/issues/301

vijithassar commented 2 years ago

maybe d3.single() for the method name?

curran commented 2 years ago

Latest implementation for reference:

const one = (selection, name, className) =>
  selection
    .selectAll(name + '.' + className)
    .data([null])
    .join(name)
    .attr('class', className);

I find myself copying this into codebases often, so thought I'd post it here for easy reference.

curran commented 1 year ago

An interesting alternative solution https://the-politico.github.io/nicar2019_reactive-d3/parts/idempotence/

Fil commented 1 year ago

see discussion on #300

curran commented 1 month ago

Added to https://github.com/curran/d3-rosetta