Skip to content
Advertisement

D3 update color based on data

I’m drawing and coloring lines based on data with D3. I now want to update the color of these lines based on different features in the same data set but my color-changing function (colorP2) does not work–I know this color change looks useless, but it will later be triggered by a button.

Any ideas? Below is my code.

[Update] There was an error in the code that was unrelated to my question, as pointed out by Andrew Reed, which I fixed and marked in the code below.

index.html

<html>
<head>
<style>
.line {
    stroke-width: 4px;
    fill: none;
}
</style>
</head>

<script src="https://d3js.org/d3.v6.min.js"></script>
<script type="module">
  import {drawLines} from './drawLines.js';

  d3.json("test.geojson").then(drawLines);
</script>

<body>
    <svg id='map'></svg>
    <button onclick="colorP1()">colorP1</button>
    <button onclick="colorP2()">colorP2</button>
    <!-- <svg id="data" class="map_frame"></svg> -->

</body>
</html>

drawLines.js

function colorInterpolate(data, property) {
    let max_d = d3.max(data.features.map(d => d.properties[property]));
    let range = [max_d, 1];
    return d3.scaleSequential().domain(range).interpolator(d3.interpolateViridis); 
}

export function drawLines(data) {

    let width = 900,
        height = 500,
        initialScale = 1 << 23,
        initialCenter = [-74.200698022608137, 40.034504451003734]

    let svg = d3.select('#map')
        .attr('height', height)
        .attr('width', width)

    let projection = d3.geoMercator()
        .scale(initialScale)
        .center(initialCenter)
        .translate([width / 2, height / 2])

    let path = d3.geoPath(projection)

    let myColor = colorInterpolate(data, 'p1');

    let lines = svg.append('g')

    lines.selectAll('path')
        .data(data.features)
        .join('path') // previously wrong, error was unrelated to question, as pointed out by Andrew.
        .attr('class', 'line')
        .attr('d', path)
        .attr("stroke", function(d) {
                return myColor(d.properties.p1);
            })

    colorP2();

    function colorP2() {
        let myColor = colorInterpolate(data, 'p2');
        lines.selectAll('path')
            .data(data.features)
            .join()
            .attr("stroke", function(d) {
                    return myColor(d.properties.p2);
                })
    }
}

test.geojson

{
"type": "FeatureCollection",
"name": "lines",
"crs": { "type": "name", "properties": { "name": "urn:ogc:def:crs:OGC:1.3:CRS84" } },
"features": [
{ "type": "Feature", "properties": { "id": 3, "p1": 1, "p2": 3}, "geometry": { "type": "LineString", "coordinates": [ [ -74.201304101157845, 40.033790926216739 ], [ -74.201226425025339, 40.033761910802717 ], [ -74.201164135201353, 40.033738641825124 ] ] } },
{ "type": "Feature", "properties": { "id": 4, "p1": 2, "p2": 2}, "geometry": { "type": "LineString", "coordinates": [ [ -74.200521185229846, 40.034804885753857 ], [ -74.200535458528648, 40.034780636493231 ], [ -74.200698022608137, 40.034504451003734 ], [ -74.200932444446437, 40.034106179618831 ], [ -74.201017665586349, 40.033961391736824 ] ] } }
]
}

Advertisement

Answer

Solution

Ultimately, you don’t need to join any data in the color changing function: the elements already exist with the data bound to them. The join is intended to ensure one DOM element exists for every item in your data array. Instead, just select the elements and change their attributes/styles:

  lines.selectAll('path')
       .attr("stroke", function(d) { return myColor(d.properties.p2); })

Problem

I have a strong suspicion that you haven’t quite shared your exact code – if you have, nothing should be drawn as the paths will be placed in a non-valid SVG element: <undefined></undefined>.

Normally you could use a join to reselect elements (even if it was not needed), because it returns both enter and update selections. But you aren’t using selection.join() correctly here. When first adding the paths, you’d normally specify the type of element you want to join as the parameter passed to .join, rather than using selection.append():

       selection.join('path')

Not specifying the type of element you want to create will create an element as follows: <undefined></undefined>. The source code shows how elements are entered in the join statement:

 enter.append(onenter + ""); 

Where onenter is the first parameter passed to .join.

As you haven’t specified a valid SVG element, SVG doesn’t know how to render it, or its children (the path):

var svg = d3.select("svg");

var rects = svg.selectAll("rect")
  .data([1,2])
  .join()
  .append("rect")
  .attr("x", d=>d*100+50)
  .attr("y", 100)
  .attr("width", 30)
  .attr("height", 30)
  .attr("fill","crimson");
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/6.2.0/d3.min.js"></script>
<svg></svg>

Ultimately you should be using .join("path") – for comparison, the following breaks down what happens with each of:

  • selection.join()
  • selection.join().append(“p”)
  • selection.join(“p”);

// Don't specify any tag:
var a = d3.select("div")
  .selectAll(null) // this doesn't affect the type of element entered in the join
  .data([1])
  .join()
  .text("a");
  
console.log(".join() :", a.node(), "parent:", a.node().parentNode);

// join without specifying a tag, then append
var b = d3.select("div")
  .selectAll(null)
  .data([1])
  .join()
  .append("p")
  .text("b");
  
console.log(".join().append('p') : ", b.node(), "parent:", b.node().parentNode);

// Specify the type of element to join (the correct method):
var c = d3.select("div")
  .selectAll(null)
  .data([1])
  .join("p")
  .text("c");
  
console.log(".join('p') :", c.node(), "parent:", c.node().parentNode);
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/6.2.0/d3.min.js"></script>
<div></div>
User contributions licensed under: CC BY-SA
5 People found this is helpful
Advertisement