When drag one node, the links should be updated with new coordinates. current code do not clear the old path. I use join method with the group element but group element not clean up sub-elements appended.
svg_arc()
function svg_arc() {
var svg = d3.select('body')
.append('svg')
.attr('width',410)
.attr('height',500)
//.style('border','10px solid red')
.style('background','#ececec')
var g = svg.append('g')
.attr('stroke','black')
.attr('stroke-width',1)
.attr('fill','black')
var data = [
{
id:"pointA",
x:100,
y:350,
r:6,
text:'A'
},{
id:"pointB",
x:250,
y:50,
r:6,
text:'B'
},{
id:"pointC",
x:400,
y:350,
r:6,
text:'C'
}
]
var node = g.selectAll('.node')
.data(data)
.join('g')
.attr('class','node')
.attr('transform',d => `translate(${d.x},${d.y})`)
.attr("pointer-events", "all")
var circle = node.append('circle')
.attr('id',d => d.id)
.attr('cx',0)
.attr('cy',0)
.attr('r',d => d.r)
.attr("pointer-events", "all")
var text = node.append('text')
.attr('class','text')
.text(d => d.text)
.attr('x',0)
.attr('y',0)
.attr('dx',-15)
.attr('dy',-15)
node.call(
d3.drag()
.on("start",function(event,d) {
d3.select(this).raise().classed("active", true);
})
.on("drag",function(event,d) {
d.x += event.dx
d.y += event.dy
update_links(g,data)
d3.select(this).attr('transform',`translate(${d.x},${d.y})`)
})
.on("end", function dragEnd(event,d) {
d3.select(this).classed("active", false);
})
)
update_links(g,data)
}
function update_links(g,n) {
var data = [0]//dummy data
var line = g.selectAll('.lines')
.data(data)
.join('g')
.attr('class','lines')
line.append('path')
.attr('class','AB')
.attr('d',['M',n[0].x,n[0].y,'L',n[1].x,n[1].y].join(' '))
.attr('stroke','black')
line.append('path')
.attr('class','BC')
.attr('d',['M',n[1].x,n[1].y,'L',n[2].x,n[2].y].join(' '))
.attr('stroke','black')
line.append('path')
.attr('class','AC')
.attr('d',['M',n[0].x,n[0].y,'Q',n[1].x,n[1].y,n[2].x,n[2].y].join(' '))
.attr('stroke','black')
.attr('fill','none')
line.append('path')
.attr('class','MID')
.attr('d',['M',(n[0].x+n[1].x)/2,(n[0].y+n[1].y)/2,'L',(n[1].x+n[2].x)/2,(n[1].y+n[2].y)/2].join(' '))
.attr('fill','none')
}
<script src="https://unpkg.com/d3#7.0.4/dist/d3.min.js"></script>
You're using join for the group, that's correct, but then you are just appending the paths to the group. In other words, you need an enter/update/exit selection for the paths themselves.
Here's the code with that change:
svg_arc()
function svg_arc() {
var svg = d3.select('body')
.append('svg')
.attr('width', 410)
.attr('height', 500)
//.style('border','10px solid red')
.style('background', '#ececec')
var g = svg.append('g')
.attr('stroke', 'black')
.attr('stroke-width', 3)
.attr('fill', 'black')
var data = [{
id: "pointA",
x: 100,
y: 350,
r: 6,
text: 'A'
}, {
id: "pointB",
x: 250,
y: 50,
r: 6,
text: 'B'
}, {
id: "pointC",
x: 400,
y: 350,
r: 6,
text: 'C'
}]
var node = g.selectAll('.node')
.data(data)
.join('g')
.attr('class', 'node')
.attr('transform', d => `translate(${d.x},${d.y})`)
.attr("pointer-events", "all")
var circle = node.append('circle')
.attr('id', d => d.id)
.attr('cx', 0)
.attr('cy', 0)
.attr('r', d => d.r)
.attr("pointer-events", "all")
var text = node.append('text')
.attr('class', 'text')
.text(d => d.text)
.attr('x', 0)
.attr('y', 0)
.attr('dx', -15)
.attr('dy', -15)
node.call(
d3.drag()
.on("start", function(event, d) {
d3.select(this).raise().classed("active", true);
})
.on("drag", function(event, d) {
d.x += event.dx
d.y += event.dy
update_links(g, data)
d3.select(this).attr('transform', `translate(${d.x},${d.y})`)
})
.on("end", function dragEnd(event, d) {
d3.select(this).classed("active", false);
})
)
update_links(g, data)
}
function update_links(g, n) {
var data = [0] //dummy data
var group = g.selectAll('.lines')
.data(data)
.join('g')
.attr('class', 'lines');
const line = group.selectAll(".line")
.data([0, 1, 2, 3])
.join("path")
.attr("class", "line")
.attr("d", d => d === 0 ? ['M', n[0].x, n[0].y, 'L', n[1].x, n[1].y].join(' ') :
d === 1 ? ['M', n[1].x, n[1].y, 'L', n[2].x, n[2].y].join(' ') :
d === 2 ? ['M', n[0].x, n[0].y, 'Q', n[1].x, n[1].y, n[2].x, n[2].y].join(' ') : ['M', (n[0].x + n[1].x) / 2, (n[0].y + n[1].y) / 2, 'L', (n[1].x + n[2].x) / 2, (n[1].y + n[2].y) / 2].join(' '))
.attr('stroke', 'black')
.attr('fill', d => d > 1 ? 'none' : null)
}
<script src="https://unpkg.com/d3#7.0.4/dist/d3.min.js"></script>
Have in mind that this is just to show you why your code was behaving the way it was. There are still lots and lots of problems to fix here, from just unnecessary bits to patterns that effectively worsen performance (for instance, the very use of join inside the drag callback is a big no) , but that's out of the scope of this answer.
Related
I am completely lost trying to spawn circles with pictures inside that all move within a force.drag. They should be created based on a list of arrays. Any help is really appreciated!
This is my code so far:
var data2 = [
{id: 1, name: "Sachin", pop: 200, x: 0, y: 0, color: 'red', image: "https://picsum.photos/900" },
{id: 2, name: "Murali", pop: 100, x: 200, y: 200, color: 'green', image: "https://random.imagecdn.app/500/500" }
]
var body = d3.select("body")
var svg = body.append("svg")
.attr("width", 800)
.attr("height", 800)
data2.forEach(function(d){
svg.append("clipPath")
.attr('id', d.id)
.append('circle')
.attr("cx", d.x + d.pop /2)
.attr("cy", d.y + d.pop / 2)
.attr("r", d.pop /2)
.attr("", console.log(d))
.style("fill", "green")
.attr("", console.log("done! with this one"))
})
data2.forEach(function(d){
svg.append('image')
.attr('xlink:href',d.image)
.attr('width', d3.sum([d.pop]))
.attr('height', d3.sum([d.pop]))
.attr('x', d.x)
.attr('y', d.y)
.attr('clip-path','url(#' + d.id + ')');
});
But this only gives me this:
But I am trying to make this...
and applying force to it as shown in this snippet:
var data2 = [
{id: 1, name: "Sachin", pop: 20, color: 'red', image: "https://picsum.photos/900" },
{id: 2, name: "Murali", pop: 10, color: 'green', image: "https://random.imagecdn.app/500/500" }
]
var width = 400//Dimensions.get('window').width,
var height = 400//Dimensions.get('window').height,
// create SVG
var body = d3.select("body") //SVG ÄR HELA ARBETSYTAN
var svg = body.append("svg")
.attr("width", width)
.attr("height", height)
//.style("background", "#000000")
// init a force layout, using the nodes and edges in dataset
var force = d3.layout.force()
.nodes(data2)
//.links(data.edges)
.size([width, height])
.charge([-300]) //.linkDistance([100])
.start()
// define 10 random colors
//var colors = d3.scale.category10()*/
var drums = svg.selectAll("circle")
.data(data2)
.enter()
.append("circle")
drums.attr("cx", function(d) { return width/2 }) //för att vara olika för varje item.
.attr("cy", height/2)
.attr("r", function(d) { return d.pop })
.attr("", function(d) { console.log(d) })
.call(force.drag)
drums.style("fill", function(d) {
if (!d.image) {
return ("url(#" + d.id + ")"); //console.log(d.image)
}else{
return d.color
}
})
force.on("tick", function() {
(svg.selectAll('circle') //drums
.attr("cx", function(d) { return d.x })
.attr("cy", function(d) { return d.y })
)
})
drums.on("click", function(d) {
console.log(d)
d3.select(this)
.attr("r" , d3.sum([d3.select(this).attr("r"),10]))
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.0.4/d3.min.js"></script>
Thank you so much for taking a look into this! :)
I am learning d3.js, and I have this problem:
The following code in d3 basically draws a bar chart, with an update button that is sorting the data once in a descending order and once in ascending order. Also, numeric labels appear on the bars.
I would like to transition the numeric label from the current value to the updated value. For example, if the first bar has a numeric label of 20, and the new updated value after sorting is 100, I would like this label to transition from 20 to 100 as (20, 21, ..., 100) during the specific transition time, and vice versa, if the original label is 100 and the updated value is 20 the transition goes as 100, 99, ..., 20.
I know I can just transition the numeric value with the bar, but I would like to know how to do the transition from the current numeric value to the new update value as an exercise.
const data = [
{key: 0, value: 50},
{key: 1, value: 20},
{key: 2, value: 100},
{key: 3, value: 30},
{key: 4, value: 40},
{key: 5, value: 70}
]
// const dataset = [50, 20, 100, 30, 40]
const svgWidth = 800;
const svgHeight = 400;
const xScale = d3.scaleBand()
.domain(d3.range(data.length))
.rangeRound([0, svgWidth])
.paddingInner(0.1);
const yScale = d3.scaleLinear()
.domain([0, d3.max(data, d => d.value)])
.range([0, svgHeight]);
const svg = d3.select('#chart')
.append('svg')
.attr('width', svgWidth)
.attr('height', svgHeight);
let bars = svg.selectAll('rect').data(data, d => d.key);
let labels = svg.selectAll('text').data(data);
bars.enter()
.append('rect')
.each(function(d){return this._old = d;})
.attr('width', xScale.bandwidth)
.attr('height', d => yScale(d.value))
.attr('fill', d => `rgb(${d.value}, ${d.value * 2}, ${d.value * 3})`)
.attr('x', (d, i) => xScale(i))
.attr('y', d => svgHeight - yScale(d.value))
.attr('stroke', 'black')
.attr('stroke-width', 3)
labels.enter()
.append('text')
.attr('x', (d, i) => xScale(i) + (xScale.bandwidth() / 2))
.attr('y', d => svgHeight - yScale(d.value) + 20)
.attr('font-size', 20)
.attr('text-anchor', 'middle')
.attr('fill', 'white')
.text(d => d.value);
let asc = false;
d3.select('button').on('click', () => {
if(!asc){
data.sort((a,b) => b.value - a.value );
}else{
data.sort((a,b) => a.value - b.value );
};
asc = !asc;
bars = svg.selectAll('rect').data(data, d => d.key);
labels = svg.selectAll('text').data(data);
bars
.transition()
.delay((d, i) => (i * 10))
.duration(3000)
.each(function(d){return this._old = d;})
.attr('x', (d, i) => xScale(i))
.attr('height', d => yScale(d.value))
.attr('y', d => svgHeight - yScale(d.value));
labels
.transition()
.delay((d, i) => (i * 10))
.duration(3000)
.tween("text", function(d) {
var that = this;
var i = d3.interpolate(0, d.value); // Number(d.percentage.slice(0, -1))
return function(t) {
d3.select(that).text(i(t).toFixed(0));
}
})
.attr('y', d => svgHeight - yScale(d.value) + 20);
})
I found the "tween" function included in the above code for a similar but not exactly the same question. I don't know how to make the interpolation start from the current value instead of 0. I know I need somehow to store the old value, and access it in the tween, but not sure how.
Another question regarding the tween function: why do we assign var that = this and select that in the returned function?
Thanks in advance
You can get the current value for each text by different ways.
For instance, with vanilla JavaScript:
var current = +(this.textContent);
Or using a D3 getter:
var current = +(d3.select(this).text());
Here is your code with that change:
const data = [{
key: 0,
value: 50
},
{
key: 1,
value: 20
},
{
key: 2,
value: 100
},
{
key: 3,
value: 30
},
{
key: 4,
value: 40
},
{
key: 5,
value: 70
}
]
// const dataset = [50, 20, 100, 30, 40]
const svgWidth = 800;
const svgHeight = 400;
const xScale = d3.scaleBand()
.domain(d3.range(data.length))
.rangeRound([0, svgWidth])
.paddingInner(0.1);
const yScale = d3.scaleLinear()
.domain([0, d3.max(data, d => d.value)])
.range([0, svgHeight]);
const svg = d3.select('body')
.append('svg')
.attr('width', svgWidth)
.attr('height', svgHeight);
let bars = svg.selectAll('rect').data(data, d => d.key);
let labels = svg.selectAll('text').data(data);
bars.enter()
.append('rect')
.each(function(d) {
return this._old = d;
})
.attr('width', xScale.bandwidth)
.attr('height', d => yScale(d.value))
.attr('fill', d => `rgb(${d.value}, ${d.value * 2}, ${d.value * 3})`)
.attr('x', (d, i) => xScale(i))
.attr('y', d => svgHeight - yScale(d.value))
.attr('stroke', 'black')
.attr('stroke-width', 3)
labels.enter()
.append('text')
.attr('x', (d, i) => xScale(i) + (xScale.bandwidth() / 2))
.attr('y', d => svgHeight - yScale(d.value) + 20)
.attr('font-size', 20)
.attr('text-anchor', 'middle')
.attr('fill', 'white')
.text(d => d.value);
let asc = false;
d3.select('button').on('click', () => {
if (!asc) {
data.sort((a, b) => b.value - a.value);
} else {
data.sort((a, b) => a.value - b.value);
};
asc = !asc;
bars = svg.selectAll('rect').data(data, d => d.key);
labels = svg.selectAll('text').data(data);
bars
.transition()
.delay((d, i) => (i * 10))
.duration(3000)
.each(function(d) {
return this._old = d;
})
.attr('x', (d, i) => xScale(i))
.attr('height', d => yScale(d.value))
.attr('y', d => svgHeight - yScale(d.value));
labels
.transition()
.delay((d, i) => (i * 10))
.duration(3000)
.tween("text", function(d) {
var current = +(d3.select(this).text());
var that = this;
var i = d3.interpolate(current, d.value); // Number(d.percentage.slice(0, -1))
return function(t) {
d3.select(that).text(i(t).toFixed(0));
}
})
.attr('y', d => svgHeight - yScale(d.value) + 20);
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<button>Click</button>
<br>
I have the following pie chart with a very nice transition:
http://plnkr.co/edit/b4uLimUSZzxiPzAkNpBt?p=preview
The code for the pie chart is the following:
function addPieChart(meas, dataFile) {
var width = 400,
height = 400,
radius = Math.min(width, height) / 2.1,
color = d3.scale.ordinal()
.range(["#016da9", "#4c96d5"])
.domain([0, 1]);
//check if the svg already exists
var plot = d3.select("#svgPIEChart")
if (plot.empty()) {
var vis = d3.select("#pieChart")
.append("svg") //create the SVG element
.attr({
id: "svgPIEChart"
})
} else {
var vis = d3.select("#svgPIEChart")
vis.selectAll("*").remove();
};
//svg element
vis.attr({
//set the width and height of our visualization (these will be attributes of the <svg> tag
width: width,
height: height
});
//group of the svg element
var svg = vis
.append("g")
.attr({
'transform': "translate(" + width / 2 + "," + height * .52 + ")"
});
var arc = d3.svg.arc()
.startAngle(function(d) {
return d.x;
})
.endAngle(function(d) {
return d.x + d.dx;
})
.outerRadius(function(d) {
return (d.y + d.dy) / (radius);
})
.innerRadius(function(d) {
return d.y / (radius);
});
d3.text(dataFile, function(text) {
var csv = d3.csv.parseRows(text);
var json = buildHierarchy(csv);
// it seems d3.layout.partition() can be either squares or arcs
var partition = d3.layout.partition()
.sort(null)
.size([2 * Math.PI, radius * radius])
.value(function(d) {
return d.Revenue;
});
var path = svg.data([json]).selectAll(".theArc")
.data(partition.nodes)
.enter()
.append("path")
.attr("class", "theArc") //<<<<<<<<<<new jq
.attr("id", function(d, i) {
return "theArc_" + i;
}) //Give each slice a unique ID //<<<<<<<<<<new jq
.attr("display", function(d) {
return d.depth ? null : "none";
})
.attr("d", arc)
.style("stroke", "#fff")
.style("fill", function(d) {
return color((d.children ? d : d.parent).name);
})
.attr("fill-rule", "evenodd")
.style("opacity", 1)
.on("mouseover", mouseover)
.on("mouseout", mouseout)
.on("click", up)
.each(stash);
// path.each(stash).on("click", up)
//this is a start>>>>>
path
.append("title") //mouseover title showing the figures
.text(function(d) {
return d.name + ": " + formatAsShortInteger(d.Revenue);
});
//>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
svg.data([json]).selectAll(".theTxts")
.data(partition.nodes)
.enter()
.append("text")
.attr("class", "theTxts")
.attr("dx", 10) //Move the text from the start angle of the arc
.attr("dy", 15) //Move the text down
.append("textPath")
.attr("class", "foo")
.attr("xlink:href", function(d, i) {
return "#theArc_" + i;
})
.text(function(d) {
if ((d.name != 'root') && ((d.name != 'B T') || (currentMeasure != 'W'))) {
return d.name;
}
})
.on("click", up) //<<<new jq;
svg.append("text")
.attr({
x: "0",
y: "0",
'class': "title",
"id": "titleX",
'text-anchor': "middle"
})
.text(currentMeasure + " split")
//>>
.append("tspan")
.attr({
dy: "1.1em",
x: 0,
'text-anchor': "middle"
})
.text("for " + latestMth)
.attr("class", "title");
d3.selectAll("input").on("change", function change() {
value = createValueFunc(this.value);
currentMeasure = this.value;
var path2 = svg.data([json]).selectAll(".theArc");
path2
.data(partition.value(value).nodes)
.transition()
.duration(1500)
.attrTween("d", arcTween)
//to update the tooltips
svg.selectAll("title")
.text(function(d) {
return d.name + ": " + formatAsShortInteger(value(d));
});
svg.selectAll("textPath")
.text(function(d) {
if ((d.name != 'root') && ((d.name != 'B T') || (currentMeasure != 'W'))) {
return d.name;
}
});
// the following deletes what was originally created and then recreates the text
svg.selectAll("#titleX").remove();
svg.append("text")
.attr({
x: "0",
y: "0",
'class': "title",
"id": "titleX",
'text-anchor': "middle"
})
.text(currentMeasure + " split")
.append("tspan")
.attr({
dy: "1.1em",
x: 0,
'text-anchor': "middle"
})
.text("for " + latestMth)
.attr("class", "title");
addProdTitl();
updateTOPbarChart(currentGrp, currentMeasure, currentColr);
updateBOTbarChart(currentGrp, currentMeasure, currentColr);
});
function mouseover() {
d3.select(this)
.transition()
.duration(200)
.style("opacity", 0.9);
};
function mouseout() {
d3.select(this)
.transition()
.duration(200)
.style("opacity", 1);
};
// update bar chart when user selects piece of the pie chart
function up(d, i) {
currentGrp = d.name;
// (d.children ? d : d.parent).name
currentColr = color((d.children ? d : d.parent).name); // color(d.name);
addProdTitl();
updateTOPbarChart(d.name, currentMeasure, currentColr); //color(d.name));
updateBOTbarChart(d.name, currentMeasure, currentColr); //color(d.name));
};
// Stash the old values for transition.
function stash(d) {
d.x0 = d.x;
d.dx0 = d.dx;
};
// Interpolate the arcs in data space.
function arcTween(a) {
var i = d3.interpolate({
x: a.x0,
dx: a.dx0
}, a);
return function(t) {
var b = i(t);
a.x0 = b.x;
a.dx0 = b.dx;
return arc(b);
};
};
});
};
If you change the selection of the radio button and then click on a section of the pie chart that section does not complete its transition as I believe it decides it needs to move on to its "on" event.
How do I ensure that the transition completes? (and the pie chart does not do a pac-man impersonation)
Clear your listeners when the tranistion starts.
Attach it when the transition is ended
Like this:
path2
.data(partition.value(value).nodes)
.transition()
.duration(1500)
.attrTween("d", arcTween)
.each("start", function(){
d3.select(this)
.on("mouseover", null) //CLEARING the listeners
.on("mouseout", null) //CLEARING the listeners
.on("click", null) //CLEARING the listeners
})
.each("end", function(){
d3.select(this)
.on("mouseover", mouseover) //attaching the listeners
.on("mouseout", mouseout) //attaching the listeners
.on("click", up) ////attaching the listeners
});
working code here
How can I make a basic connected graph (two nodes and a link connecting them for example) that doesn't use a force() layout? I just want to be able to drag a node and have the link adjust to stay connected as a node is being dragged. I dont want any of the charge or positioning capabilities of force(). Essentially I want every node to be "sticky". Nodes will only move when being dragged.
But is there a simple way to do this? Every example I have seen is built around a force directed graph.
I've looked at this example, http://bl.ocks.org/mbostock/3750558 , but it starts with a force directed graph then makes the nodes sticky. This approach seems backwards for what I want.
Is there a basic example somewhere?
I have made a small code snippet. Hope this is helpful.
var data = {
nodes: [{
name: "A",
x: 200,
y: 150
}, {
name: "B",
x: 140,
y: 300
}, {
name: "C",
x: 300,
y: 300
}, {
name: "D",
x: 300,
y: 180
}],
links: [{
source: 0,
target: 1
}, {
source: 1,
target: 2
}, {
source: 2,
target: 3
}, ]
};
var c10 = d3.scale.category10();
var svg = d3.select("body")
.append("svg")
.attr("width", 1200)
.attr("height", 800);
var drag = d3.behavior.drag()
.on("drag", function(d, i) {
d.x += d3.event.dx
d.y += d3.event.dy
d3.select(this).attr("cx", d.x).attr("cy", d.y);
links.each(function(l, li) {
if (l.source == i) {
d3.select(this).attr("x1", d.x).attr("y1", d.y);
} else if (l.target == i) {
d3.select(this).attr("x2", d.x).attr("y2", d.y);
}
});
});
var links = svg.selectAll("link")
.data(data.links)
.enter()
.append("line")
.attr("class", "link")
.attr("x1", function(l) {
var sourceNode = data.nodes.filter(function(d, i) {
return i == l.source
})[0];
d3.select(this).attr("y1", sourceNode.y);
return sourceNode.x
})
.attr("x2", function(l) {
var targetNode = data.nodes.filter(function(d, i) {
return i == l.target
})[0];
d3.select(this).attr("y2", targetNode.y);
return targetNode.x
})
.attr("fill", "none")
.attr("stroke", "white");
var nodes = svg.selectAll("node")
.data(data.nodes)
.enter()
.append("circle")
.attr("class", "node")
.attr("cx", function(d) {
return d.x
})
.attr("cy", function(d) {
return d.y
})
.attr("r", 15)
.attr("fill", function(d, i) {
return c10(i);
})
.call(drag);
svg {
background-color: grey;
}
<script src="https://d3js.org/d3.v3.min.js"></script>
Gilsha has a great answer, but note that newer versions of d3 no longer use the behavior module.
Instead of this:
var drag = d3.behavior.drag()
.on("drag", function(d, i) {
d.x += d3.event.dx
d.y += d3.event.dy
d3.select(this).attr("cx", d.x).attr("cy", d.y);
links.each(function(l, li) {
if (l.source == i) {
d3.select(this).attr("x1", d.x).attr("y1", d.y);
} else if (l.target == i) {
d3.select(this).attr("x2", d.x).attr("y2", d.y);
}
});
});
Simply change d3.behavior.drag() to d3.drag()
var drag = d3.drag()
.on("drag", function(d, i) {
d.x += d3.event.dx
d.y += d3.event.dy
d3.select(this).attr("cx", d.x).attr("cy", d.y);
links.each(function(l, li) {
if (l.source == i) {
d3.select(this).attr("x1", d.x).attr("y1", d.y);
} else if (l.target == i) {
d3.select(this).attr("x2", d.x).attr("y2", d.y);
}
});
});
What's the easiest way to update model with d3 tree layout.
here's the example
http://jsfiddle.net/mnk/vfro9tkz/
var data = {
name: 'Music',
children: [{
name: 'Rock',
children: [{
name: 'Two'
}, {
name: 'Three',
children: [{
name: 'A'
}, {
name: 'Bonjourallo'
}, {
name: 'Coco coco coco coco'
}]
}, {
name: 'Four'
}]
}, {
name: 'Rap',
children: [{
name: 'Hip-Hop/Rap'
}]
}]
};
var svg = d3.select('body').append('svg');
svg.attr('width', 700)
.attr('height', 400);
var tree = d3.layout.tree().size([600, 300]);
function update(model) {
var nodes = tree.nodes(model);
var links = tree.links(nodes);
nodes.forEach(function(d, i) {
d.index = d.parent ? d.parent.children.indexOf(d) : 0;
d.width = getNameLength(d.name);
if (!hasNephewOrChildren(d)) {
d.x = getHorizontalPosition(d)
d.y = (d.parent ? d.parent.y : 0) + 40;
d.mode = 'horizontal';
} else {
d.x = d.depth * 60;
d.y = getVerticalPosition(d);
d.mode = 'vertical';
}
});
var node = svg.selectAll('.node')
.data(nodes)
.enter()
.append('g').attr('class', 'node')
.attr('opacity', 1)
.attr('visibility', function(d) {
return d.depth ? 'visible' : 'hidden'
})
.attr('transform', function(d, i) {
return 'translate(' + d.x + ',' + d.y + ')'
});
var lineFunction = d3.svg.line()
.x(function(d) {
return d.x;
})
.y(function(d) {
return d.y;
})
.interpolate("linear");
var paths = svg.selectAll('g.node').append("path")
.attr("d", function(d) {
return lineFunction(generatePath(d));
})
.attr("stroke", "#aaa")
.attr("stroke-width", 1)
.attr("fill", "none");
function generatePath(d) {
var points = [];
if (d.depth > 1) {
if (d.mode === 'horizontal') {
points.push({
x: d.parent.x - d.x + d.parent.width,
y: -25
});
points.push({
x: d.width / 2,
y: -25
});
points.push({
x: d.width / 2,
y: 0
});
} else {
points.push({
x: d.parent.x - d.x + d.parent.width / 2,
y: d.parent.y - d.y + 30
});
points.push({
x: d.parent.x - d.x + d.parent.width / 2,
y: 15
});
points.push({
x: d.parent.x - d.x + d.parent.width / 2 + 15,
y: 15
});
}
}
return points;
}
node.append('rect')
.attr('class', 'rect')
.attr('width', function(d, i) {
return d.width
})
.attr('height', 30)
.attr('rx', 15)
node.append('text')
.text(function(d) {
return d.name
})
.attr('x', 10)
.attr('y', 20);
var close = node.append('g')
.attr('class', 'remove-icon-group')
.on('click', function(d) {
console.log('todo remove d and all childrens.');
// update(model);
})
.attr('transform', function(d, i) {
return 'translate(' + (d.width - 15) + ',15)'
});
close.append('circle')
.attr('class', 'remove-icon')
.attr('r', 10)
close.append('line')
.attr('x1', -4)
.attr('x2', 4)
.attr('y1', -4)
.attr('y2', 4)
.attr('stroke', '#a0a0a0')
.attr('stroke-width', 1);
close.append('line')
.attr('x1', 4)
.attr('x2', -4)
.attr('y1', -4)
.attr('y2', 4)
.attr('stroke', '#a0a0a0')
.attr('stroke-width', 1);
}
update(data);
function getLastDescendant(d) {
if (d.children && d.children.length) {
return getLastDescendant(d.children[d.children.length - 1]);
}
return d;
}
function hasNephewOrChildren(d) {
var siblings = d.parent ? d.parent.children : [d];
var hasChildren = false;
siblings.forEach(function(sibling) {
if (sibling.children && sibling.children.length) {
hasChildren = true;
}
});
return hasChildren;
}
function getHorizontalPosition(d) {
if (d.index === 0) {
return d.parent ? d.parent.x + 60 : 0;
}
var prevSibling = d.parent.children[d.index - 1];
return prevSibling.x + prevSibling.width + 10;
}
function getVerticalPosition(d) {
var prevY = (d.parent ? d.parent.y : -40);
if (d.index) {
var prevSibling = d.parent.children[d.index - 1];
var lastDescendant = getLastDescendant(prevSibling);
prevY = lastDescendant.y;
}
return prevY + 40;
}
function getNameLength(str) {
var length = str.length * 8;
return length < 60 ? 60 + 30 : length + 30;
}
You're very close. You already have all the drawing code extracted to update, and there's a place where you've commented out that you need to call it again. You need to figure out how to modify the model in response to user clicks, and then call update with the new model.
The thing you'll encounter is that when you call update again, some DOM nodes will already be onscreen. That is, the enter selection will be empty, but the update selection will not be. The simplest, and ugliest, way to handle this is to remove and re-add all the nodes:
svg.selectAll('.node').remove();
svg.selectAll('.node')
.data(nodes)
.enter()
.append("g") // and so on
The better way to do it is explained in the General Update Pattern (be sure to see all three). You should also read the last paragraph of the .enter() docs.