-
Notifications
You must be signed in to change notification settings - Fork 87
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
Implement Layer#remove
#109
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -49,12 +49,19 @@ suite("d3.layer", function() { | |
sinon.spy(entering, "transition"); | ||
return entering; | ||
}); | ||
var remove = this.remove = sinon.spy(); | ||
var base = this.base = d3.select("#test").append("svg"); | ||
|
||
this.layer = base.layer({ | ||
dataBind: dataBind, | ||
insert: insert | ||
}); | ||
|
||
this.layerWithRemove = this.base.append("g").layer({ | ||
dataBind: dataBind, | ||
insert: insert, | ||
remove: remove | ||
}); | ||
}); | ||
|
||
test("invokes the provided `dataBind` method exactly once", function() { | ||
|
@@ -82,7 +89,30 @@ suite("d3.layer", function() { | |
this.layer.draw([]); | ||
assert(this.insert.calledOn(this.dataBind.returnValues[0].enter.returnValues[0])); | ||
}); | ||
|
||
test("by default removes exiting nodes from the DOM", function() { | ||
this.layer.draw([1]); | ||
assert.equal(this.layer.selectAll("g").size(), 1); | ||
this.layer.draw([]); | ||
assert.equal(this.layer.selectAll("g").size(), 0); | ||
}); | ||
test("invokes the provided `remove` method in the context of the layer's bound 'exit' selection", function() { | ||
var updating, exiting; | ||
|
||
this.layerWithRemove.draw([1, 2, 3]); | ||
this.layerWithRemove.draw([]); | ||
|
||
updating = this.dataBind.returnValues[1]; | ||
exiting = updating.exit.returnValues[0]; | ||
|
||
assert(this.remove.calledOn(exiting)); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. So, I think it's premature to assume that drawing an empty set is equivalent to removing the layer. I can see, for example, visualizing streaming data might result in gaps that would just not have any nodes in the rendering cycle for that subset, but that might change come next data frame. I think this also adds a bit of unstated "magic" whereby we're saying calling There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In the issue that prompted this change (#63), we identified an asymmetry between how data-bound elements are inserted and how they are removed. In the former case, d3.chart requires a function dedicated to the task of insertion. In the latter case, users are expected to remove data-bound elements in a lifecycle event. In light of that, I don't believe this patch suffers from the problem you are describing here. Can you confirm? If so, I think this is indicative of a confusing method name, and we should talk about finding a better name! There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. So is the assumption that What do you think? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think this is definitely indicative of a naming issue. As proposed, "remove" describes functionality for each data-bound element individually when its associated data point is removed from the set. I can see how its generic name and association with the Layer instance itself suggests that the Layer instance is the target. Unfortunately, a consistent solution will involve a breaking API change. This is because "insert" already suffers from the problem you are identifying (in the same way, it is not the insertion logic for the Layer instance, but the insertion logic for each new data-bound element). What we would need is some clear, concise nomenclature for "data-bound element". I'm going to use the word "point" for demonstration purposes (even though I admit that is a terrible choice). The two methods might be better named d3.select('svg').append('g').layer({
dataBind: myDataBindFunction,
insertPoint: myFunctionForInsertingASingleNewDataBoundElement,
removePoint: myFunctionForRemovingASingleDataBoundElement
}); If we found a better name than "point", do you think this would clear things up? |
||
}); | ||
test("does not invoke `remove` method when `exit` selection is empty", function() { | ||
this.layerWithRemove.draw([1, 2, 3]); | ||
this.layerWithRemove.draw([1, 2, 3]); | ||
|
||
assert.equal(this.remove.callCount, 0); | ||
}); | ||
|
||
suite("event triggering", function() { | ||
test("invokes event handlers with the correct selection", function() { | ||
var layer = this.base.append("g").layer({ | ||
|
@@ -137,19 +167,25 @@ suite("d3.layer", function() { | |
}, | ||
insert: function() { | ||
return this.append("g"); | ||
}, | ||
remove: function() { | ||
return this.remove(); | ||
} | ||
}); | ||
var enterSpy = sinon.spy(); | ||
var updateSpy = sinon.spy(); | ||
var exitSpy = sinon.spy(); | ||
layer.draw([1]); | ||
|
||
layer.on("enter", enterSpy); | ||
layer.on("update", updateSpy); | ||
layer.on("exit", exitSpy); | ||
|
||
layer.draw([1]); | ||
|
||
sinon.assert.callCount(enterSpy, 0); | ||
sinon.assert.callCount(updateSpy, 1); | ||
sinon.assert.callCount(exitSpy, 0); | ||
}); | ||
|
||
suite("Layer#off", function() { | ||
|
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.
What's the reasoning behind calling remove on the selection if it's not empty? I might be missing some context here.
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.
This is for consistency with how we invoke the lifecycle events--see this logic in
Layer#draw
....but it's not really fair to answer a question like, "Why are we doing this here?" with, "because we're doing it over there." The underlying motivation was originally discussed in this issue: #69 .
Does this seem right to you?
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.
So I think the way I would have approached it, is by doing the selection check here:
https://github.com/jugglinmike/d3.chart/blob/gh-103-extensions/src/layer.js#L217
and here:
https://github.com/jugglinmike/d3.chart/blob/gh-103-extensions/src/layer.js#L227
What I worry about is the
remove
routine having more destructive behavior (like removing parent containers that the selection would go into) that you might not want to execute unless you're effectively 'done' redrawing this layer or done with the chart itself (or perhaps you want to hide it somehow.)Is there a reason we couldn't localize it to the specific lifecycle events and just not call the handlers?
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 think there's a crossed wire here. Let's discuss this in person.