Converting a D3.js forcediagram from version 5 to 6.1, I write:
let drag = simulation => {
function dragstarted(event, d) {
if (!event.active) simulation.alphaTarget(0.3).restart();
d.fx = d.x;
d.fy = d.y;
}
function dragged(event,d) {
d.fx = event.x;
d.fy = event.y;
}
function dragended(event,d) {
if (!event.active) simulation.alphaTarget(0);
d.fx = null;
d.fy = null;
}
// release of fixed positions
function dblclick(d) {
d.fx = null;
d.fy = null;
}
return d3.drag()
.on("start", dragstarted)
.on("drag", dragged)
.on("end", dragended)
.on("dblclick", dblclick);
However, I am getting the error on dblclick:
Uncaught (in promise) Error: unknown type: dblclick d3.min.js:2:9279
The code is copied from: https://observablehq.com/#d3/force-directed-graph?collection=#d3/d3-force. Only the dblclick is added. It is working in version 5, but as a separate function.
How can I add dblclick in version 6.1 of D3.js the correct way?
First, there is no "dblclick" typename for drag.on, which is not the same of selection.on. Hence your error:
Error: unknown type: dblclick
The only valid typenames are: "start", "drag" and "end". That said, your code should not work, be it v5 or v6.
The rest of this answer deals with another major problem with D3 v6:
As specified in the API, selection.on() in D3 version 6 passes the event as the first argument:
When a specified event is dispatched on a selected element, the specified listener will be evaluated for the element, being passed the current event (event) and the current datum (d), with this as the current DOM element (event.currentTarget)
Therefore, your function should be:
function dblclick(_, d) {
d.fx = null;
d.fy = null;
}
Here, the _ is just a JavaScript convention telling us that the first parameter (the event) is never used.
Related
The bounty expires in 2 days. Answers to this question are eligible for a +50 reputation bounty.
Tom Rudge wants to draw more attention to this question.
I've created a D3 force directed graph in an Angular environment using typescript. I've followed this "brushing" example of a selectable force directed graph code for this example here. I've had to convert it into typescript and obviously fit it into my structure which is pretty standard D3. Brushing basically allows the selection of multiple nodes on a graph. This is triggered by keyup/keydown of the shift key.
Clicking on a node selects it and de-selects everything else.
Shift-clicking on a node toggles its selection status and leaves all
other nodes as they are.
Shift-dragging toggles the selection status of all nodes within the
selection area.
Dragging on a selected node drags all selected nodes.
Dragging an unselected node selects and drags it while de-selecting
everything else.
I've struggled to get this fully working.. here are my issues:
Shift toggles brushing on and off fine, but when I begin dragging when shift is active, when I keydown shift(let go) brushing remains on.
The brushing box does not appear until my second drag/selection attempt (as it remains on).
When I do brush/select nodes these don't get the selected class appended to them.
Here is a stackBlitz to my Angular D3 brushing attempt.
I'm relatively new to D3 so any assistance welcomed!
Brushing part of the code, for full version please see demo:
let gBrushHolder = svg.append('g');
let gBrush = null;
svg.style('cursor', 'grab');
let brushMode = false;
let brushing = false;
const brushstarted = () => {
// keep track of whether we're actively brushing so that we
// don't remove the brush on keyup in the middle of a selection
brushing = true;
node.each(function (d) {
d.previouslySelected = this.shiftKey && d.selected;
});
};
const brushed = () => {
if (!_d3.event.sourceEvent) return;
if (!_d3.event.selection) return;
console.log("during")
var extent = _d3.event.selection;
node.classed('selected', function (d) {
return (d.selected =
d.previouslySelected ^
(<any>(
(extent[0][0] <= d.x &&
d.x < extent[1][0] &&
extent[0][1] <= d.y &&
d.y < extent[1][1])
)));
});
};
const brushended = () => {
if (!_d3.event.sourceEvent) return;
if (!_d3.event.selection) return;
if (!gBrush) return;
gBrush.call(brush.move, null);
if (!brushMode) {
// the shift key has been release before we ended our brushing
gBrush.remove();
gBrush = null;
}
brushing = false;
console.log("end")
};
let brush = _d3
.brush()
.on('start', brushstarted)
.on('brush', brushed)
.on('end', brushended);
const keydown = () => {
this.shiftKey = _d3.event.shiftKey;
if (this.shiftKey) {
// if we already have a brush, don't do anything
if (gBrush) return;
brushMode = true;
if (!gBrush) {
gBrush = gBrushHolder.append('g');
gBrush.call(brush);
}
}
};
const keyup = () => {
this.shiftKey = false;
brushMode = false;
if (!gBrush) return;
if (!brushing) {
console.log("NOT BRUSHING")
// only remove the brush if we're not actively brushing
// otherwise it'll be removed when the brushing ends
gBrush.remove();
gBrush = null;
}
};
_d3.select('body').on('keydown', keydown);
_d3.select('body').on('keyup', keyup);
Why does the following work:
var line = d3.line()
.x(function(d) { return d.x; })
.y(function(d) { return d.y; });
var path = d3.select("#frame")
.append("path")
.attr("class", "line")
.attr("d", line(nodes.data()))
But if I directly integrate the function without declaring it first like this it doesn't work (It doesn't draw the line). Why is that? What do I have to change for it to work?
var path = d3.select("#frame")
.append("path")
.attr("class", "line")
.attr(
"d",
function() {
d3.line()
.x(function() { return nodes.data().x })
.y(function() { return nodes.data().y });
}
);
The code
var line = d3.line()
.x(function(d) { return d.x; })
.y(function(d) { return d.y; });
is creating a line generator. A line generator is a function. If you call this function and pass it your data array, then it returns the a string for the "d" attribute of an SVG path, which defines the shape of the line.
The line generator needs to know how to get the x and y coordinates for a given element in your data array. This is defined by the functions that you pass to line.x() and line.y(). These functions get called individually for each element in your data array.
In your code, function(d) { return d.x; } and function(d) { return d.y; } will be called on each element in the data array in order to get the x and y coordinates for that element. The argument to these functions, d, is a single element from your data array. It is not the entire data array.
If you don't want to define the line generator in a separate variable, then you could do the following:
var path = d3.select("#frame")
.append("path")
.attr("class", "line")
.attr("d", d3.line()
.x(function(d) { return d.x; })
.y(function(d) { return d.y; })(nodes.data())
);
This replaces line in your first block of code with the entire line generator.
Your code doesn't work for two reasons. First, your line generator is
d3.line()
.x(function() { return nodes.data().x })
.y(function() { return nodes.data().y })
The functions that you are passing to x() and y() do not work correctly. These functions should define how to get the x and y coordinates for a single element in your data array, like function(d) { return d.x; }. The argument, d, is one element from your data array.
Second, in the code that you have, the line generator is never called on your data array. You don't need to put the line generator inside of another function, but if you did then it would look like this:
function() {
return d3.line()
.x(function(d) { return d.x; })
.y(function(d) { return d.y; })(nodes.data());
}
But there's no need for this outer function. You can just do what I showed above.
I have the following backbone model with a d3.drag functionality. I cannot call the model's this inside the d3's context.
I came across with solutions for similar questions by defining a variable model=this and calling by model.draw.. but how can I add it inside d3's drag?
DataMapper.Models.Anchor = Backbone.Model.extend({
defaults: {
//...
},
initialize : function(){
d3.select("#anchor").call(this.dragAnchor); //make the #anchor draggable
},
dragAnchor: d3.drag()
.on("start", function (d) {
console.log("something"); //it prints
var thisDragY = this.drawSomething(a,b,c);
// this.drawSomething is not a function
// because inside d3.drag(), 'this' refers to #anchor
// what I want to refer is the model
})
.on("drag", function (d) {})
.on("end", function (d) {}),
drawSomething: function (parent, cx, cy) {
//code
}
});
Is there a way to use underscore's bind to achieve my desired goal? Link to a useful article.
The solution was found by a team member - to call the drag as a function.
DataMapper.Models.Anchor = Backbone.Model.extend({
defaults: {
//...
},
initialize : function(){
d3.select("#anchor").call(this.dragAnchor()); //make the #anchor draggable
},
dragAnchor: function(){
var self=this;
return d3.drag()
.on("start", function (d) {
console.log("something"); //it prints
var thisDragY = self.drawSomething(a,b,c);
})
.on("drag", function (d) {})
.on("end", function (d) {}),
drawSomething: function (parent, cx, cy) {
//code
}
});
I know there are many questions here about how to globalize the scope of variables that have been updated inside a json callback function. And I have been trying to work off those examples for days now but I cant seem to work out how I can adapt that to my code when my json callbacks are on an event eg.on right click(context menu). I have been trying to use this example but have not been successful to try and adapt it to my code:
function myFunc(data) {
console.log(data);
}
d3.json('file.json', function (data) {
var json = data;
myFunc(json);
}
I am working with a scatterplot in d3. My first connection just adds two rows of my table into both of the arrays. Each row represents a dot. When I click on one of these dots, a connection is made to the database and links to that selected dot appear on the graph as other dots. And these are added to the baseData array and the libraryData remains the same. WHen I right click on one of these dots that dot is added to the libraryData array.But as indicated in the code, instances of libraryData outside of the function has not been updated. Below is my code
var libraryData = [];
var baseData = [];
d3.json("connection4.php", function(error,dataJson) {
dataJson.forEach(function(d) {
d.YEAR = +d.YEAR;
d.counter = +d.counter;
libraryData.push(d);
baseData.push(d);
})
var circles = svg.selectAll("circle")
.data(libraryData) // libraryData here remains unchanged even after librayData has been updated in the function below!
.enter()
.append("circle")
.attr("class", "dot")
.attr("r", 3.5)
.attr("cx", function(d) {return x(YearFn(d))})
.attr("cy", function(d) {return y(Num_citationsFn(d))})
.style("fill","blue")
.on("click", clickHandler)
function clickHandler (d, i) {
d3.json("connection2.php?paperID="+d.ID, function(error, dataJson) {
dataJson.forEach(function(d) {
d.YEAR = +d.YEAR;
d.counter = +d.counter;
baseData.push(d);
})
var circles = svg.selectAll("circle")
.data(baseData)
.enter()
.append("circle")
.attr("class", "dot")
.attr("r", 3.5)
.attr("cx", function(d) {return x(YearFn(d))})
.attr("cy", function(d) {return y(Num_citationsFn(d))})
.style("fill", "red")
.on("contextmenu", rightClickHandler);
})
function rightClickHandler (d, i) {
d3.json("connection6.php?paperID="+d.ID, function(error, dataJson) {
})
d3.select(this)
.style("fill", "blue");
libraryData.push(d);
console.log(libraryData);// updated
}
console.log(libraryData)// not updated
});
I am new to d3 and I would appreciate any help and feedback thanks in advance!!
It's not scope, it's a timing issue
function rightClickHandler (d, i) {
d3.json("connection6.php?paperID="+d.ID, function(error, dataJson) {
})
d3.select(this)
.style("fill", "blue");
libraryData.push(d);
console.log("I bet I'm second", libraryData);// updated
}
console.log("I bet I'm first", libraryData)// not updated
});
thefunction(error, dataJson) in the json call only executes once json is returned from your php function which can take a while. Meanwhile, your program performs the second call to console.log runs straight away and before the function in the json call has added anything to libraryData.
Run the above with the changes to console.log to see
Basically anything you want to do that depends on library data being updated must be called inside function(error, dataJson)
I am trying to make a bubble chart, in that if i click on a bubble, the title of the bubble should appear in the console. I tried some ways, but was not successful.
d3.json("deaths.json",
function (jsondata) {
var deaths = jsondata.map(function(d) { return d.deaths; });
var infections = jsondata.map(function(d) { return d.infections; });
var country = jsondata.map(function(d) { return d.country; });
var death_rate = jsondata.map(function(d) { return d.death_rate; });
console.log(deaths);
console.log(death_rate);
console.log(infections);
console.log(country);
console.log(date);
//Making chart
for (var i=0;i<11;i++)
{
var f;
var countryname=new Array();
var dot = new Array();
dot = svg.append("g").append("circle").attr("class", "dot").attr("id",i)
.style("fill", function(d) { return colorScale(death_rate[i]); }).call(position);
//adding mouse listeners....
dot.on("click", click());
function click()
{
/***********************/
console.log(country); //i need the title of the circle to be printed
/*******************/
}
function position(dot)
{
dot .attr("cx", function(d) { return xScale(deaths[i]); })
.attr("cy", function(d) { return yScale(death_rate[i]); })
.attr("r", function(d) { return radiusScale(infections[i]); });
dot.append("title").text(country[i]);
}
}
});
I need the title of circle to be printed
Please help!!!
You had the good idea by using the on("click", ...) function. However I see two things:
The first problem is that you don't call the function on the click event but its value. So, you write dot.on("click", click()); instead of dot.on("click", click);. To understand the difference, let's imagine that the function click needs one argument, which would for example represent the interesting dot, what would it be? Well, you would write the following:
dot.on("click", function(d){click(d)})
Which is equivalent (and less prone to errors) to writing:
dot.on("click", click)
Now, the second point is that, indeed you want to pass the node as an argument of the function click. Fortunately, with the on event, as I used in my example, the function click is called with the argument d which represents the data of dot. Thus you can now write:
dot.on("click", click);
function click(d)
{
console.log(d.title); //considering dot has a title attribute
}
Note: you can also use another argument by writing function click(d,i) with i representing the index in the array, see the documentation for more details.
If you have a title on your data,
dot.on('click' , function(d){ console.log(d.title); });