D3 v7 two types of brushes - javascript

I am trying to implement in D3 v7 a dual-brush functionality, which:
with the standard mouse button brush allows to highlight the brushed dots in a scatter chart (as has been done many times)
with the mouse button brush while holding the control key (or the right mouse button or any other easy to implement key), allows to remove (ie make transparent) brushed dots in a scatter chart.
Functionality 1) already works but I struggle to add 2) at the same time.
In the post
Brush d3.js with right click
the brush.filter functionality is discussed as a potential solution at high level. So I thought about implementing it in the following manner:
var brush = d3.brush()
.filter(event => !event.ctrlKey)
.on("end", brushed) //Function to highlight
svg
.call(brush)
and
var brush2 = d3.brush()
.filter(event => event.ctrlKey)
.on("end", brushed2) //Function to make dots transparent
svg
.call(brush2)
It appears though that the svgs are drawn above each other an consequently only the last added layer is available to brush.
No luck either with something like
var brush = d3.brush()
.filter(event => !event.ctrlKey)
.on("end", brushed)
.filter(event => event.ctrlKey)
.on("end", brushed2)
svg
.call(brush) //Function with an if statement to decide highlight or removal
Understand I might need to specify "pointer-events","none" somewhere but haven't been able to fully figure this out by myself.
Any ideas welcome.

Related

Vertical line in D3 plot move on zoom/pan

I am trying to draw a vertical line marker in my graph in D3. It is modeled off of this example: https://bl.ocks.org/mbostock/34f08d5e11952a80609169b7917d4172
My issue is that after I draw my line, it doesn't move as I zoom/scroll the graph. An example is shown below:
Currently, I have it calculated as a d3.area().
this.pastDateArea = d3.area()
.x(function(d) { return this.x(this.props.pastDate.toDate()) }.bind(this))
.y0(0)
.y1(function(d) { return this.height }.bind(this))
It is appended as
var pastDateData = [{x:this.props.pastDate.toDate(), y:150}]
this.focus.append("path")
.datum(pastDateData)
.attr("class", "area")
.attr("d", this.pastDateArea)
and zoomed/brushed using
//zoom
var t = d3.event.transform;
this.x.domain(t.rescaleX(this.x2).domain());
//brush
this.svg.select(".zoom").call(this.zoom.transform, d3.zoomIdentity
.scale(this.width / (s[1] - s[0]))
.translate(-s[0], 0));
I know there are similar questions to this one (namely, Draw a vertical line representing the current date in d3 gantt chart) but none of them include the zooming/panning features I have in my graph.
Please let me know if you need more information and thanks!
The issue is that you are not updating the vertical bar with each zoom event. Using the code of the example you show, several things are done when the chart is zoomed, including as you note:
x.domain(t.rescaleX(x2).domain()); // update x scale
focus.select(".area").attr("d", area); // redraw chart area
While you do give the new area the class of area, d3.select will only pick the first matching element. So, on zoom, only one .area element is updated (the first encountered, generally the first appended). But, replacing this with d3.selectAll(".area") will not generate the intended results as the area function referenced (.attr("d",area) ) is only used for the first area (that of the graph, not of the vertical bar).
A solution is to select each area (the chart and the bar) independently and update the area with their respective area generators. To do so, append the vertical bar with a unique class name, or an id and use that to select it later. Then when updating the graph on zoom or brush you can use:
x.domain(s.map(x2.invert, x2)); // update x scale
focus.select(".area").attr("d", area); // redraw chart area
focus.select(".bar").attr("d", pastDateArea);// redraw vertical bar
Remember that this needs to be done for both zoom and brush. Also, in the given example, a clip path is assigned in the css for .area, so you need to keep that in mind as well.
Here's a modified example.

Remove Double-Click Zoom D3

I have a stripped down force directed graph with zoom/pan capability: https://bl.ocks.org/anonymous/36b27a0b6f8c485c25995e7c223def3c
Here is the zoom portion:
var zoomHandler = d3.zoom()
.on("zoom", zoomActions);
zoomHandler(svg);
function zoomActions(){
g.attr("transform", d3.event.transform)
}
I would like to remove the zoom capability for double-click, and only keep it for scrolling. Based on the docs and other questions, I know I must include .on("dblclick.zoom", null) somewhere. However, I cannot figure out where to place this piece of code. In addition, I'm not sure if I am properly using "svg" and "g" to group my elements, which may be contributing to the issue. I have seen the other questions related to this issue, and have not been able to fit them into my situation.
How should I remedy this? Thank you for any insight you all might have!
Since you are calling the zoom function on the svg selection...
zoomHandler(svg);
... you have to add the listener to the same selection:
svg.on("dblclick.zoom", null)
Here is your updated bl.ocks: https://bl.ocks.org/anonymous/42745557a8602692d9dc98c33a327d29
var zoomHandler = d3.zoom()
.on("zoom", zoomActions);
zoomHandler(svg);
svg.call("zoom").on("dblclick.zoom", null);
function zoomActions(){
g.attr("transform", d3.event.transform)
}

Disable clearing of D3.js brush

I would like to use a D3.js brush to allow users to select a range of values on an axis. By default, clicking outside the brush clears it, so that no range is selected.
However, I would like to adjust this behaviour so that clicking outside the brush doesn't alter the brush extent. In effect, there should be no way to clear the brush, some range should always be selected.
I believe I have to hook into the brush event somehow to disable the clearing, but I don't really know how to go about that.
Here's an example of the kind of interface I'm talking about (Fiddle). When you click to the left or right of the black bar, the brush is cleared and the bar disappears.
How can I disable this behaviour?
d3 brush by design calls 'brushmove()' once a user presses a mouse on the brush element (i.e. on 'mousedown.brush' event).
If effectively leads to resetting the previous brush extent.
A possible workaround is to replace the original mousedown.brush handler with the custom one. The custom handler will only call the original handlers once the mouse was moved after initial mousedown.
var brushNode = chart.append("g")
.call(brush);
brushNode
.selectAll("rect")
.attr("y", -10)
.attr("height", 10);
// store the reference to the original handler
var oldMousedown = brushNode.on('mousedown.brush');
// and replace it with our custom handler
brushNode.on('mousedown.brush', function () {
brushNode.on('mouseup.brush', function () {
clearHandlers();
});
brushNode.on('mousemove.brush', function () {
clearHandlers();
oldMousedown.call(this);
brushNode.on('mousemove.brush').call(this);
});
function clearHandlers() {
brushNode.on('mousemove.brush', null);
brushNode.on('mouseup.brush', null);
}
})
See the demo.

Auto or Random Scatterplot Matrix Selections d3.js

I would like to know if there is a way to have some sort of random selection, or any selection happening when this loads and to stop when a user interacts with it. AS it stands now, people do not realize that they can interact with the chart.
Maybe the top left box could have a selection being drawn and receding?
http://mbostock.github.io/d3/talk/20111116/iris-splom.html
Thanks for any help or suggestions!
You could draw the brush programatically using the extent() method.
also, look at some of the examples in this discussion.
Using the same example you posted, add this to the end of the csv callback:
var e = [[0.4,1.4],[1.4,2.4]]; //set brush range
brush.extent(e);
cell.call(brush); //draw brush
and if you want the brush to simulate user interaction:
cell.select(".extent")
.transition()
.attr('width',20).attr('height',20)
.attr('x',10).attr('y',10)
.transition()
.attr('width',80).attr('height',80);

Is there a D3 layout engine like force layout but with click event support

I want to draw a directed graph where any node can link to any other node (ie no defined hierarchy) and I was using the force layout engine but it doesn't support click event handling. I want to be able to click on a node and have that node centered and everything else laid out around it.
Is that possible in D3?
EDIT:
According to the API documentation for d3.force.layout:
force.on(type, listener)
Registers the specified listener to receive events of the specified
type from the force layout. Currently, only "tick" events are
supported
which suggests that simply adding a click event handler will not work.
Also, a tree layout needs (as far as I know) a hierarchy and my data is more tangled.
After puzzling away at this, I have something close to what I wanted so I'll share.
Firstly, There is no click handler for the layout engine but that is not needed for this; I wanted to click on a node and have that become fixed so I need a click handler on the node.
Secondly, there is a "fixed" property at the node level described in the d3 API for the force layout engine.
Combining these two, I can add click handler to the node that locks or unlocks the node's position.
Using the example here I add the following to the CSS:
circle:hover { fill: red; }
and change the circle definition from:
var circle = svg.append("svg:g").selectAll("circle")
.data(force.nodes())
.enter().append("svg:circle")
.attr("r", 6)
.call(force.drag);
to
var circle = svg.append("svg:g").selectAll("circle")
.data(force.nodes())
.enter().append("svg:circle")
.attr("r", 6)
.on("click", function(d){ d.fixed = 1 - d.fixed; force.start(); })
.call(force.drag);
and now, when I mouse-over a circle, it turns red (showing me I captured it) and then if I click, it locks it in place. I can then repeat this on other circles and drag them to where I want.

Categories