Setting CSS Custom Properties with d3 on each element

I am working with a svg element & the initial markup is following

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<script type="text/javascript" src="https://d3js.org/d3.v7.min.js"></script>
	
<body>
<svg xmlns="http://www.w3.org/2000/svg" height="400" width="450">
  <path class="line1" d="M 100 350 l 150 -300" stroke="red" stroke-width="3" fill="none" />
  <path class="line2" d="M 250 50 l 150 300" stroke="blue" stroke-width="3" fill="none" />
  <path class="line3" d="M 175 200 l 150 0" stroke="green" stroke-width="3" fill="none" />
  <path class="line4" d="M 100 350 q 150 -300 300 0" stroke="magenta" stroke-width="5" fill="none" />

</svg>
</body>
</html>

I want to call getTotalLength on each path and set that as CSS custom properties for each path.

By using vanilla, I can do this

document.querySelectorAll("[class^='line']")
    .forEach(
        (a, i) => {
            a.style.setProperty('--pathLength', a.getTotalLength());
        }
    );

which gives me

I was wondering, how can I replicate this in d3. So far, I tried this which is doing the job but I am doing the same selection twice.

d3.selectAll("[class^='line']")
    .style('--pathLength', (d, i) => {
        const dataset = d3.selectAll("[class^='line']");
        return `${dataset['_groups'][0][i].getTotalLength()}`
    });

Is there a better way to achieve this?

The full code is following

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<script type="text/javascript" src="https://d3js.org/d3.v7.min.js"></script>
	
<body>

<svg xmlns="http://www.w3.org/2000/svg" height="400" width="450">
  <path class="line1" d="M 100 350 l 150 -300" stroke="red" stroke-width="3" fill="none" />
  <path class="line2" d="M 250 50 l 150 300" stroke="blue" stroke-width="3" fill="none" />
  <path class="line3" d="M 175 200 l 150 0" stroke="green" stroke-width="3" fill="none" />
  <path class="line4" d="M 100 350 q 150 -300 300 0" stroke="magenta" stroke-width="5" fill="none" />

</svg>
<script type="text/javascript">
<!--vanilla-->
document.querySelectorAll("[class^='line']")
    .forEach(
        (a, i) => {
            a.style.setProperty('--pathLengthVanilla', a.getTotalLength());
        }
    );
<!--d3-->	
d3.selectAll("[class^='line']")
    .style('--pathLengthD3', (d, i) => {
        const dataset = d3.selectAll("[class^='line']");
        return `${dataset['_groups'][0][i].getTotalLength()}`
    });	
</script>
</body>

</html>

In D3, the selection.style method takes a property name ("--pathLength") and a callback, which is invoked with the element as the value of this, so you can return this.getTotalLength():

d3.selectAll("[class^='line']")
  .style("--pathLength", function (d, i) {
    return this.getTotalLength();
  });

Note that, for the value of this to be the element, you can’t use arrow functions, which don’t get their own this. That is, you can’t write (d, i) => { ... }, like you did in your example; you have to write function(d, i) { ... }.

Here’s an example:

1 Like

@tophtucker many thanks for this. However, I have a follow-up question. I am new to d3 and currently experimenting with d3 API.

I have conducted the following experiment to get more clarification on use of d and this

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<script type="text/javascript" src="https://d3js.org/d3.v7.min.js"></script>

<body>

    <svg xmlns="http://www.w3.org/2000/svg" height="400" width="450">
  <path class="line1" d="M 100 350 l 150 -300" stroke="red" stroke-width="3" fill="none" />
  <path class="line2" d="M 250 50 l 150 300" stroke="blue" stroke-width="3" fill="none" />
  <path class="line3" d="M 175 200 l 150 0" stroke="green" stroke-width="3" fill="none" />
  <path class="line4" d="M 100 350 q 150 -300 300 0" stroke="magenta" stroke-width="5" fill="none" />

</svg>
    <script type="text/javascript">
        /*vanilla*/
        document.querySelectorAll("[class^='line']")
            .forEach(
                (a, i) => {
                    a.style.setProperty('--pathLengthVanilla', a.getTotalLength());
                }
            );
        /*d3*/
        d3.selectAll("[class^='line']")
            .style('--pathLengthD3', (d, i) => {
                const dataset = d3.selectAll("[class^='line']");
                return `${dataset['_groups'][0][i].getTotalLength()}`
            });

        /*perform dynamic transform on the existing element by selecting first*/
        d3.selectAll("[class^='line']").attr('transform', function(d, i) {
            const attributeX = parseFloat(this.getAttribute('d').match(/(?<=M )\d+/gm)) / 10;
            return `translate(${attributeX})`
        })


        /*create data driven circle and dynamic transform on the new element based on the dataset*/
        d3.select('svg')
            .selectAll('circle')
            .data(d3.selectAll("[class^='line']"))
            .enter()
            .append('circle')
            .attr('cx', '0')
            .attr('cy', (d, i) => {
                return '5'
            })
            .attr('r', '5')
            .attr('transform', (d, i) => {
                    const attributeX = parseFloat(d.getAttribute('d').match(/(?<=M )\d+/gm)) / 10
                    return `translate(${attributeX})`
                }

            )
    </script>
</body>

</html>

As you can notice, line 38 requires this and line 55 requires d

Is it kindly possible for you to shed some light on this as to when to use this and when to use
d.
From this example, it is explanatory that this is required for modifying existing elements and d is used for the new data-driven element.

Since I am new to d3, I am not sure if this is the correct inference ? I would love to hear some explanation from you.

I can never keep this straight myself so I often myself using a console.log to examine those things. For example, you might type:

.attr('some_attribute', function(a,b) {
  console.log([a,b,this])
  ...
}

That way, you can simply examine the contents of those variables and decide how to act accordingly.

1 Like