Schmoo
Schmoo

Reputation: 574

Data Join with Custom Key does not work as expected

I am plotting some points using d3. I want to change the shape off all the points based on some condition. The join looks a bit like this:

var data=[{x:10,y:10}, {x:20, y:30}];
var shape = "rect";
...
var point = svg.selectAll(".point")
  .data(data, function(d, idx) { return "row_" + idx + "_shape_" + shape;})
;

The d3 enter() and exit() selections do not seem to reflect any changes caused by "shape" changing.

Fiddle is here: http://jsfiddle.net/schmoo2k/jcpctbty/

Upvotes: 2

Views: 1108

Answers (1)

Cool Blue
Cool Blue

Reputation: 6476

You need to be aware that the key function is calculated on the selection with this as the SVG element and then on the data with the data array as this.

I think maybe this is what you are trying to do...

var data = [{
  x: 10,
  y: 10
}, {
  x: 20,
  y: 30
}];

var svg = d3.select("body").append("svg")
  .attr("width", 500)
  .attr("height", 500);

function update(data, shape) {
  var point = svg.selectAll(".point")
    .data(data, function(d, idx) {
      var key = "row_" + idx + "_shape_" + (Array.isArray(this) ? "Data: " + shape :
        d3.select(this).attr("shape"));
      alert(key);
      return key;
    });

  alert("enter selection size: " + point.enter().size());

  point.enter().append(shape)
    .attr("class", "point")
    .style("fill", "red")
    .attr("shape", shape);
  switch (shape) {
    case "rect":
      point.attr("x", function(d) {
          return d.x;
        })
        .attr("y", function(d) {
          return d.y;
        })
        .attr("width", 5)
        .attr("height", 5);
      break;
    case "circle":
      point.attr("cx", function(d) {
          return d.x;
        })
        .attr("cy", function(d) {
          return d.y;
        })
        .attr("r", 5);
      break;
  }
  point.exit().remove();
}

update(data, "rect");

setTimeout(function() {
  update(data, "circle");
}, 5000);
text {
  font: bold 48px monospace;
}
.enter {
  fill: green;
}
.update {
  fill: #333;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.5/d3.js"></script>


Abstracted version

Just to tidy things up here is a more readable and idiomatic version (including fixing a problem with the text element)...

var data = [{
  x: 10,
  y: 10,
}, {
  x: 20,
  y: 30,
}];

var svg = d3.select("body").append("svg")
  .attr("width", 500)
  .attr("height", 500),
  marker = Marker();

function update(data, shape) {
  var point = svg.selectAll(".point")
    .data(data, key("shape", shape)),

    enter = point.enter().append("g")
    .attr("class", "point")
    .attr("transform", function(d) {
      return "translate(" + d.x + "," + d.y + ")"
    })
    .attr("shape", shape);

  enter.append(shape)
    .style("fill", "red")
    .attr(marker.width[shape], 5)
    .attr(marker.height[shape], 5);

  enter.append("text")
    .attr({
      "class": "title",
      dx: 10,
      "text-anchor": "start"
    })
    .text(shape);

  point.exit().remove();
}

update(data, "rect");

setTimeout(function() {
  update(data, "circle");
}, 2000);

function Marker() {
  return {
    width: {
      rect: "width",
      circle: "r"
    },
    height: {
      rect: "height",
      circle: "r"
    },
    shape: function(d) {
      return d.shape
    },
  };
}

function key(attr, value) {
  //join data and elements where value of attr is value
  function _phase(that) {
    return Array.isArray(that) ? "data" : "element";
  }

  function _Type(that) {
    return {
      data: value,
      get element() {
        return d3.select(that).attr(attr)
      }
    }
  }
  return function(d, i, j) {
    var _value = _Type(this)
    return i + "_" + _value[_phase(this)];
  };
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>


Generalised, data-driven approach

var data = [{
  x: 10,
  y: 10,
}, {
  x: 20,
  y: 30,
}];

var svg = d3.select("body").append("svg")
  .attr("width", 500)
  .attr("height", 500),
  marker = Marker();

function update(data, shape) {
  //data-driven approach
  data.forEach(function(d, i) {
    d.shape = shape[i]
  });

  var log = [],
    point = svg.selectAll(".point")
    .data(data, key({
      shape: marker.shape,
      transform: marker.transform
    }, log)),

    //UPDATE
    update = point.classed("update", true),
    updateSize = update.size();
  update.selectAll("text").transition().duration(1000).style("fill", "#ccc");
  update.selectAll(".shape").transition().duration(1000).style("fill", "#ccc")

  //ENTER
  var enter = point.enter().append("g")
    .classed("point enter", true)
    .attr("transform", marker.dock)
    .attr("shape", marker.shape),

    //UPDATE+ENTER
    // ... not required on this occasion

    updateAndEnter = point.classed("update-enter", true);

  //EXIT
  var exit = point.exit().classed("exit", true);
  exit.selectAll("text").transition().duration(1000).style("fill", "red");
  exit.selectAll(".shape").transition().duration(1000).style("fill", "red");
  exit.transition().delay(1000.).duration(1000)
    .attr("transform", marker.dock)
    .remove();

  //ADJUSTMENTS
  enter.each(function(d) {
    //append the specified shape for each data element
    //wrap in each so that attr can be a function of the data
    d3.select(this).append(marker.shape(d))
      .style("fill", "green")
      .classed("shape", true)
      .attr(marker.width[marker.shape(d)], 5)
      .attr(marker.height[marker.shape(d)], 5)
  });

  enter.append("text")
    .attr({
      "class": "title",
      dx: 10,
      "text-anchor": "start"
    })
    .text(marker.shape)
    .style("fill", "green")
    .style("opacity", 1);

  enter.transition().delay(1000).duration(2000)
    .attr("transform", marker.transform);
}
data = generateData(40, 10)
update(data, data.map(function(d, i) {
  return ["rect", "circle"][Math.round(Math.random())]
}));

setInterval(function() {
  update(data, data.map(function(d, i) {
    return ["rect", "circle"][Math.round(Math.random())]
  }));
}, 5000);

function generateData(n, p) {
  var values = [];

  for (var i = 0; i < n; i++) {
    values.push({
      x: (i + 1) * p,
      y: (i + 1) * p
    })
  }
  return values;
};

function Marker() {
  return {
    x: {
      rect: "x",
      circle: "cx"
    },
    y: {
      rect: "y",
      circle: "cy"
    },
    width: {
      rect: "width",
      circle: "r"
    },
    height: {
      rect: "height",
      circle: "r"
    },
    shape: function(d) {
      return d.shape
    },
    transform: function(d) {
      return "translate(" + f(d.x) + "," + f(d.y) + ")"
    },
    dock: function(d) {
      return "translate(" + (d.x + 800) + "," + (d.y + 100) + ")"
    }
  };

  function f(x) {
    return d3.format(".0f")(x)
  }
}

function key(attr, value, log) {
  //join data and elements where value of attr is value
  function _phase(that) {
    return Array.isArray(that) ? "data" : "element";
  }

  function _Key(that) {
    if (plural) {
      return {
        data: function(d, i, j) {
          var a, key = "";
          for (a in attr) {
            key += (typeof attr[a] === "function" ? attr[a](d, i, j) : attr[a]);
          }
          return key;
        },
        element: function() {
          var a, key = "";
          for (a in attr) {
            key += d3.select(that).attr(a);
          }
          return key;
        }
      }
    } else {
      return {
        data: function(d, i, j) {
          return typeof value === "function" ? value(d, i, j) : value;
        },
        element: function() {
          return d3.select(that).attr(attr)
        }
      }
    }
  }

  var plural = typeof attr === "object";
  if (plural && arguments.length === 2) log = value;

  return function(d, i, j) {
    var key = _Key(this)[_phase(this)](d, i, j);
    if (log) log.push(i + "_" + _phase(this) + "_" + key);
    return key;
  };
}
text {
  font: bold 12px monospace;
  fill: black;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>

Upvotes: 2

Related Questions