I've been trying to get my head around how to update only those d3 nodes where the data has changed but I still don't have it right. In the little test example below, I am still changing everything it seems.
Am I doing this completely wrong or just a little wrong?
In the example, a click on a shape toggles the shape to be a circle or square and updates a "clickCnt" property. Then it redraws the data. It's sort of working but seems to be redrawing everything. Also clicking on the "red" shape is not working for some reason yet it is the exact same code.
var dataArray = [];
dataArray.push({ "label": 'red', "shape": "circle", "clickCnt": 0, x: 30, y: 100 });
dataArray.push({ "label": 'orange', "shape": "square", "clickCnt": 0, x: 110, y: 100 });
dataArray.push({ "label": 'yellow', "shape": "circle", "clickCnt": 0, x: 210, y: 100 });
dataArray.push({ "label": 'green', "shape": "square", "clickCnt": 0, x: 310, y: 100 });
dataArray.push({ "label": 'blue', "shape": "circle", "clickCnt": 0, x: 30, y: 200 });
dataArray.push({ "label": 'indigo', "shape": "square", "clickCnt": 0, x: 110, y: 200 });
dataArray.push({ "label": 'violet', "shape": "circle", "clickCnt": 0, x: 210, y: 200 });
dataArray.push({ "label": 'white', "shape": "square", "clickCnt": 0, x: 310, y: 200 });
var width = 400;
var height = 400;
d3.select("div#svg-container").select("svg").remove();
var svg = d3.select("#svg-container").append("svg")
.attr("width", width)
.attr("height", height);
var content = svg.append("g")
function create(data) {
var groups = content.selectAll("g")
.data(data, function (d) {
return d;
});
groups.exit().remove();
groups.enter()
.append("g")
.attr('transform', function (d, i) {
return 'translate(' + d.x + ',' + d.y + ')'
})
.each(function (d) {
var e = d3.select(this);
e.append("text")
.classed("small-text", true)
.classed("label", true)
.text(function (d) {
return d.label;
})
.style("fill", function (d) {
return d.label;
});
e.append("text")
.classed("small-text", true)
.classed("clickCnt", true)
.attr("y", 20)
.text(function (d) {
return d.clickCnt;
})
.style("fill", function (d) {
return d.label;
})
if (d.shape == "circle") {
e.append("circle")
.attr("class", "circle")
.attr("r", 15)
.attr("cx", 10)
.attr("cy", -40)
.on("click", iconClicked)
.style("cursor", "pointer");
} else if (d.shape == "square") {
e.append("rect")
.attr("class", "square")
.attr("width", 30)
.attr("height", 30)
.attr("x", 0)
.attr("y", -55)
.on("click", iconClicked)
.style("cursor", "pointer");
}
});
}
create(dataArray);
function iconClicked(evt) {
if (evt.shape == "circle") {
evt.shape = "square"
} else if (evt.shape == "square") {
evt.shape = "circle"
}
evt.clickCnt++;
document.getElementById('output').innerHTML = "item clicked: " + evt.label + " " + evt.clickCnt;
create(dataArray);
}
.circle {
stroke: red;
stroke-width: 2px;
}
.square {
stroke: blue;
stroke-width: 2px;
}
#timeline-background {
background: slategray;
}
.label {
fill: blue;
}
.small-text {
font-size: 16px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<body>
<label id="output">out</label>
<div id="timeline-background" style="width: 100%; height: 100%;">
<div id="svg-container"></div>
</div>
</body>
Your problem here is the key function when you bind the data.
If you look at the documentation, you'll see that:
A key function may be specified to control which datum is assigned to which element, replacing the default join-by-index, by computing a string identifier for each datum and element. (emphasis mine)
However, in your case, instead of using a string, you're returning a whole object:
var groups = content.selectAll("g")
.data(data, function (d) {
return d;
// ^--- this is an object
});
And, of course, this won't work.
Because of that, we have the behaviour you described: your exit selection contains all groups and they are all removed. Then, the enter selection contains all elements, and they are all painted again.
Let's see it, click the elements and have a look at the console:
var dataArray = [];
dataArray.push({ "label": 'red', "shape": "circle", "clickCnt": 0, x: 30, y: 100 });
dataArray.push({ "label": 'orange', "shape": "square", "clickCnt": 0, x: 110, y: 100 });
dataArray.push({ "label": 'yellow', "shape": "circle", "clickCnt": 0, x: 210, y: 100 });
dataArray.push({ "label": 'green', "shape": "square", "clickCnt": 0, x: 310, y: 100 });
dataArray.push({ "label": 'blue', "shape": "circle", "clickCnt": 0, x: 30, y: 200 });
dataArray.push({ "label": 'indigo', "shape": "square", "clickCnt": 0, x: 110, y: 200 });
dataArray.push({ "label": 'violet', "shape": "circle", "clickCnt": 0, x: 210, y: 200 });
dataArray.push({ "label": 'white', "shape": "square", "clickCnt": 0, x: 310, y: 200 });
var width = 400;
var height = 400;
d3.select("div#svg-container").select("svg").remove();
var svg = d3.select("#svg-container").append("svg")
.attr("width", width)
.attr("height", height);
var content = svg.append("g")
function create(data) {
var groups = content.selectAll("g")
.data(data, function (d) {
return d;
});
console.log("The exit selection size is: " + groups.exit().size())
groups.exit().remove();
groups.enter()
.append("g")
.attr('transform', function (d, i) {
return 'translate(' + d.x + ',' + d.y + ')'
})
.each(function (d) {
var e = d3.select(this);
e.append("text")
.classed("small-text", true)
.classed("label", true)
.text(function (d) {
return d.label;
})
.style("fill", function (d) {
return d.label;
});
e.append("text")
.classed("small-text", true)
.classed("clickCnt", true)
.attr("y", 20)
.text(function (d) {
return d.clickCnt;
})
.style("fill", function (d) {
return d.label;
})
if (d.shape == "circle") {
e.append("circle")
.attr("class", "circle")
.attr("r", 15)
.attr("cx", 10)
.attr("cy", -40)
.on("click", iconClicked)
.style("cursor", "pointer");
} else if (d.shape == "square") {
e.append("rect")
.attr("class", "square")
.attr("width", 30)
.attr("height", 30)
.attr("x", 0)
.attr("y", -55)
.on("click", iconClicked)
.style("cursor", "pointer");
}
});
}
create(dataArray);
function iconClicked(evt) {
if (evt.shape == "circle") {
evt.shape = "square"
} else if (evt.shape == "square") {
evt.shape = "circle"
}
evt.clickCnt++;
document.getElementById('output').innerHTML = "item clicked: " + evt.label + " " + evt.clickCnt;
create(dataArray);
}
.as-console-wrapper { max-height: 20% !important;}
.circle {
stroke: red;
stroke-width: 2px;
}
.square {
stroke: blue;
stroke-width: 2px;
}
#timeline-background {
background: slategray;
}
.label {
fill: blue;
}
.small-text {
font-size: 16px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<body>
<label id="output">out</label>
<div id="timeline-background" style="width: 100%; height: 100%;">
<div id="svg-container"></div>
</div>
</body>
(partial) Solution: Use a unique string as the returned value, such as the label property:
var groups = content.selectAll("g")
.data(data, function (d) {
return d.label;
});
Have a look:
var dataArray = [];
dataArray.push({ "label": 'red', "shape": "circle", "clickCnt": 0, x: 30, y: 100 });
dataArray.push({ "label": 'orange', "shape": "square", "clickCnt": 0, x: 110, y: 100 });
dataArray.push({ "label": 'yellow', "shape": "circle", "clickCnt": 0, x: 210, y: 100 });
dataArray.push({ "label": 'green', "shape": "square", "clickCnt": 0, x: 310, y: 100 });
dataArray.push({ "label": 'blue', "shape": "circle", "clickCnt": 0, x: 30, y: 200 });
dataArray.push({ "label": 'indigo', "shape": "square", "clickCnt": 0, x: 110, y: 200 });
dataArray.push({ "label": 'violet', "shape": "circle", "clickCnt": 0, x: 210, y: 200 });
dataArray.push({ "label": 'white', "shape": "square", "clickCnt": 0, x: 310, y: 200 });
var width = 400;
var height = 400;
d3.select("div#svg-container").select("svg").remove();
var svg = d3.select("#svg-container").append("svg")
.attr("width", width)
.attr("height", height);
var content = svg.append("g")
function create(data) {
var groups = content.selectAll("g")
.data(data, function (d) {
return d.label;
});
console.log("The exit selection size is: " + groups.exit().size())
groups.exit().remove();
groups.enter()
.append("g")
.attr('transform', function (d, i) {
return 'translate(' + d.x + ',' + d.y + ')'
})
.each(function (d) {
var e = d3.select(this);
e.append("text")
.classed("small-text", true)
.classed("label", true)
.text(function (d) {
return d.label;
})
.style("fill", function (d) {
return d.label;
});
e.append("text")
.classed("small-text", true)
.classed("clickCnt", true)
.attr("y", 20)
.text(function (d) {
return d.clickCnt;
})
.style("fill", function (d) {
return d.label;
})
if (d.shape == "circle") {
e.append("circle")
.attr("class", "circle")
.attr("r", 15)
.attr("cx", 10)
.attr("cy", -40)
.on("click", iconClicked)
.style("cursor", "pointer");
} else if (d.shape == "square") {
e.append("rect")
.attr("class", "square")
.attr("width", 30)
.attr("height", 30)
.attr("x", 0)
.attr("y", -55)
.on("click", iconClicked)
.style("cursor", "pointer");
}
});
}
create(dataArray);
function iconClicked(evt) {
if (evt.shape == "circle") {
evt.shape = "square"
} else if (evt.shape == "square") {
evt.shape = "circle"
}
evt.clickCnt++;
document.getElementById('output').innerHTML = "item clicked: " + evt.label + " " + evt.clickCnt;
create(dataArray);
}
.as-console-wrapper { max-height: 20% !important;}
.circle {
stroke: red;
stroke-width: 2px;
}
.square {
stroke: blue;
stroke-width: 2px;
}
#timeline-background {
background: slategray;
}
.label {
fill: blue;
}
.small-text {
font-size: 16px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<body>
<label id="output">out</label>
<div id="timeline-background" style="width: 100%; height: 100%;">
<div id="svg-container"></div>
</div>
</body>
Now, as you can see, the exit selection's size is always zero.
However, I wrote partial for a reason: you're not changing the elements anymore! The explanation is that you don't have a proper update selection. Since the exit selection has no element anymore, the size of the enter selection is also zero. Nothing is updated here.
Creating such update selection is beyond the scope of this answer and I'll leave that work to you.
Related
I am creating a continuous legend (have created using linear gradient). Now I want to convert same legend to look like discrete legend (have constant stepwise values). I have shared the images like how it looks now and how I want it to be looked and also the code snippet along with fiddle link
const legendColor = [{
offset: 0.0,
color: "#ff0000"
},
{
offset: 0.2,
color: "#ffff00"
},
{
offset: 0.4,
color: "#00ff00"
},
{
offset: 0.6,
color: "#00ffff"
},
{
offset: 0.8,
color: "#0000ff"
},
{
offset: 1.0,
color: "#ff00ff"
}
];
const svg = d3.select("svg");
const colorScale2 = d3.scaleLinear().domain([0, 1]).range([0, 400]);
const id = "linear-gradient-0";
const linearGradient2 = svg.append("defs")
.append("linearGradient")
.attr("id", "linear-gradient-1")
.attr("x1", "100%")
.attr("x2", "0%")
.attr("y1", "0%")
.attr("y2", "0%");
// append the color
linearGradient2
.selectAll("stop")
.data(legendColor)
.enter()
.append("stop")
.attr("offset", function(data) {
return colorScale2(data.offset) / 4 + "%";
//return data.offset + "%";
})
.attr("stop-color", function(data) {
return data.color;
});
// draw the rectangle and fill with gradient
svg.append("rect")
.attr("x", 10)
.attr("y", 88)
.attr("width", 400)
.attr("height", 20)
.style("fill", "url(#linear-gradient-1)");
Fiddle link : https://jsfiddle.net/p8mukjz9/2/
How I want it to be looked :
It's possible to change only the legendColor array the way that each color will appear twice. Each repeated color item should have an offset that is "close enough" to the next color like in example below.
You can play with this array and make the colors in any order (so red will be displayed first from the left).
const legendColor = [{
offset: 0.0,
color: "#ff0000"
},
{
offset: 0.18,
color: "#ff0000"
},
{
offset: 0.18,
color: "#ffff00"
},
{
offset: 0.34,
color: "#ffff00"
},
{
offset: 0.34,
color: "#00ff00"
},
{
offset: 0.5,
color: "#00ff00"
},
{
offset: 0.5,
color: "#00ffff"
},
{
offset: 0.66,
color: "#00ffff"
},
{
offset: 0.66,
color: "#0000ff"
},
{
offset: 0.83,
color: "#0000ff"
},
{
offset: 0.83,
color: "#ff00ff"
},
{
offset: 1.0,
color: "#ff00ff"
}
];
const svg = d3.select("svg");
const colorScale2 = d3.scaleLinear().domain([0, 1]).range([0, 400]);
const id = "linear-gradient-0";
const linearGradient2 = svg.append("defs")
.append("linearGradient")
.attr("id", "linear-gradient-1")
.attr("x1", "100%")
.attr("x2", "0%")
.attr("y1", "0%")
.attr("y2", "0%");
// append the color
linearGradient2
.selectAll("stop")
.data(legendColor)
.enter()
.append("stop")
.attr("offset", function(data) {
return colorScale2(data.offset) / 4 + "%";
//return data.offset + "%";
})
.attr("stop-color", function(data) {
return data.color;
});
// draw the rectangle and fill with gradient
svg.append("rect")
.attr("x", 10)
.attr("y", 88)
.attr("width", 400)
.attr("height", 20)
.style("fill", "url(#linear-gradient-1)");
// create tick
svg.append("g").attr("transform", "translate(10,115)").call(d3.axisBottom(colorScale2));
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/7.4.0/d3.min.js"></script>
<svg width="500"></svg>
My task is to organize a timeline visualization.
I took the example from - http://bl.ocks.org/bunkat/2338034.
Making necessary modifications I notice that timeBegin variable is equal to "0" and if I try to modify it by introducing:
d3.min(timeSegments, function (d) {
return d.segment_start;
})
The graph starts to enlarge horizontally. Only when the variable timeBegin is 0 the graph is displaying in a correct manner. Where is my wrong minding in D3?
I found another example - https://codepen.io/manglass/pen/MvLBRz, but this cases are slightly complicated for me.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>RePLICA</title>
<style type="text/css">
.chart-table {
shape-rendering: crispEdges;
}
.graph-square text {
font: 10px sans-serif;
}
div.tooltip-donut {
position: absolute;
text-align: center;
padding: .5rem;
background: #FFFFFF;
color: #313639;
border: 1px solid #313639;
border-radius: 8px;
pointer-events: none;
font-size: 1.3rem;
}
.brush .extent {
stroke: gray;
fill: dodgerblue;
fill-opacity: .365;
}
</style>
</head>
<body>
<script src="https://d3js.org/d3.v4.min.js" charset="utf-8"></script>
<script type="text/javascript">
//data
var lanes = ["Chinese", "Japanese", "Korean", "Moldova"],
laneLength = lanes.length,
timeSegments = [{
"lane": 0,
"id": "Qin",
"segment_start": 100,
"segment_end": 210,
"flag": false
},
{
"lane": 0,
"id": "Jin",
"segment_start": 210,
"segment_end": 420,
"flag": true
},
{
"lane": 0,
"id": "Sui",
"segment_start": 420,
"segment_end": 615,
"flag": false
},
{
"lane": 1,
"id": "Yamato",
"segment_start": 300,
"segment_end": 530,
"flag": false
},
{
"lane": 1,
"id": "Asuka",
"segment_start": 530,
"segment_end": 700,
"flag": true
},
{
"lane": 1,
"id": "Nara",
"segment_start": 710,
"segment_end": 800,
"flag": false
},
{
"lane": 1,
"id": "Heian",
"segment_start": 800,
"segment_end": 1180,
"flag": true
},
{
"lane": 2,
"id": "Three Kingdoms",
"segment_start": 100,
"segment_end": 670,
"flag": false
},
{
"lane": 2,
"id": "North and South States",
"segment_start": 670,
"segment_end": 900,
"flag": true
},
{
"lane": 3,
"id": "Chisinau",
"segment_start": 250,
"segment_end": 600,
"flag": false
},
{
"lane": 3,
"id": "Balti",
"segment_start": 600,
"segment_end": 900,
"flag": true
},
{
"lane": 3,
"id": "Ungheni",
"segment_start": 920,
"segment_end": 1380,
"flag": false
}
],
timeBegin = 0, // !!! d3.min(timeSegments, function (d) { return d.segment_start; }) ---- Does not work !!!
timeEnd = d3.max(timeSegments, function(d) {
return d.segment_end;
});
var widthTotal = 1300,
heightTotal = 500,
margin = {
top: 10,
right: 15,
bottom: 0,
left: 100
},
widthSVG = widthTotal - margin.right - margin.left,
heightSVG = heightTotal - margin.top - margin.bottom,
graphHeight = laneLength * 10 + heightTotal / 3; // - 3 just a coonstant
// scales
var scaleX = d3.scaleLinear()
.domain([timeBegin, timeEnd])
.range([0, widthSVG]);
var scaleY = d3.scaleLinear()
.domain([0, laneLength])
.range([0, graphHeight]);
var colorScale = d3.scaleOrdinal(d3.schemeCategory10);
var chart = d3.select("body")
.append("svg")
.attr("width", widthSVG + margin.right + margin.left)
.attr("height", heightSVG + margin.top + margin.bottom)
.attr("class", "chart-table");
var graph = chart.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")")
.attr("width", widthSVG)
.attr("height", graphHeight)
.attr("class", "graph-square");
// Draw the axis
chart.append("g")
.attr("transform", "translate(" + margin.left + ", " + (graphHeight + 20) + ")") // This controls the vertical position of the Axis
.call(d3.axisBottom(scaleX));
// Delimitation lines
graph.append("g").selectAll(".laneLines")
.data(timeSegments)
.enter().append("line")
.attr("x1", 0)
.attr("y1", function(d) {
return scaleY(d.lane);
})
.attr("x2", widthSVG)
.attr("y2", function(d) {
return scaleY(d.lane);
})
.attr("stroke", "lightgray");
// Lanes Names display
graph.append("g").selectAll(".laneText")
.data(lanes)
.enter().append("text")
.text(function(d) {
return d;
})
.attr("x", -margin.right)
.attr("y", function(d, i) {
return scaleY(i + .5);
})
.attr("dy", ".5ex")
.attr("text-anchor", "end")
.attr("class", "laneText");
// Add DIV for "hover_info"
var div = d3.select("body").append("div")
.attr("class", "tooltip-donut")
.style("opacity", 0);
// Graph item rects
graph.append("g").selectAll(".graphItem")
.data(timeSegments)
.enter().append("rect")
.attr("x", function(d) {
return scaleX(d.segment_start);
})
.attr("y", function(d) {
let shiftVertical = 9;
if (d.flag) {
shiftVertical = 0
};
return scaleY(d.lane + .5) - shiftVertical;
})
.attr("width", function(d) {
return scaleX(d.segment_end - d.segment_start);
})
.attr("height", 10)
.style("fill", function(d) {
return colorScale(d.lane);
})
// Hover effect
.on('mouseover', function(d, i) {
d3.select(this).transition()
.duration('50')
.attr('opacity', '.5');
div.transition()
.duration(50)
.style("opacity", 1);
let hover_info = ("id:" + d.id + "<br/>" + "start:" + d.segment_start + "<br/>" + "end:" + d.segment_end).toString();
//Makes the new div appear on hover:
div.html(hover_info)
.style("left", (d3.event.pageX + 10) + "px")
.style("top", (d3.event.pageY - 15) + "px");
})
.on('mouseout', function(d, i) {
d3.select(this).transition()
.duration('50')
.attr('opacity', '1')
//Makes the new div disappear:
div.transition()
.duration('50')
.style("opacity", 0);
});
</script>
</body>
</html>
The d3.min function you have does work, that's not the problem. The problem is the math you're using to calculate the width of the rectangles:
.attr("width", function(d) {
return scaleX(d.segment_end - d.segment_start);
})
As you can see, that only works if the scale starts at 0. For a more dynamic scale, like the one you want, the width should be:
.attr("width", function(d) {
return scaleX(d.segment_end - d.segment_start + scaleX.domain()[0]);
})
That is, you add the first value of the domain to the number you pass to the scale.
Here is your code with that change only:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>RePLICA</title>
<style type="text/css">
.chart-table {
shape-rendering: crispEdges;
}
.graph-square text {
font: 10px sans-serif;
}
div.tooltip-donut {
position: absolute;
text-align: center;
padding: .5rem;
background: #FFFFFF;
color: #313639;
border: 1px solid #313639;
border-radius: 8px;
pointer-events: none;
font-size: 1.3rem;
}
.brush .extent {
stroke: gray;
fill: dodgerblue;
fill-opacity: .365;
}
</style>
</head>
<body>
<script src="https://d3js.org/d3.v4.min.js" charset="utf-8"></script>
<script type="text/javascript">
//data
var lanes = ["Chinese", "Japanese", "Korean", "Moldova"],
laneLength = lanes.length,
timeSegments = [{
"lane": 0,
"id": "Qin",
"segment_start": 100,
"segment_end": 210,
"flag": false
},
{
"lane": 0,
"id": "Jin",
"segment_start": 210,
"segment_end": 420,
"flag": true
},
{
"lane": 0,
"id": "Sui",
"segment_start": 420,
"segment_end": 615,
"flag": false
},
{
"lane": 1,
"id": "Yamato",
"segment_start": 300,
"segment_end": 530,
"flag": false
},
{
"lane": 1,
"id": "Asuka",
"segment_start": 530,
"segment_end": 700,
"flag": true
},
{
"lane": 1,
"id": "Nara",
"segment_start": 710,
"segment_end": 800,
"flag": false
},
{
"lane": 1,
"id": "Heian",
"segment_start": 800,
"segment_end": 1180,
"flag": true
},
{
"lane": 2,
"id": "Three Kingdoms",
"segment_start": 100,
"segment_end": 670,
"flag": false
},
{
"lane": 2,
"id": "North and South States",
"segment_start": 670,
"segment_end": 900,
"flag": true
},
{
"lane": 3,
"id": "Chisinau",
"segment_start": 250,
"segment_end": 600,
"flag": false
},
{
"lane": 3,
"id": "Balti",
"segment_start": 600,
"segment_end": 900,
"flag": true
},
{
"lane": 3,
"id": "Ungheni",
"segment_start": 920,
"segment_end": 1380,
"flag": false
}
],
timeBegin = d3.min(timeSegments, function(d) {
return d.segment_start;
}),
timeEnd = d3.max(timeSegments, function(d) {
return d.segment_end;
});
var widthTotal = 1300,
heightTotal = 500,
margin = {
top: 10,
right: 15,
bottom: 0,
left: 100
},
widthSVG = widthTotal - margin.right - margin.left,
heightSVG = heightTotal - margin.top - margin.bottom,
graphHeight = laneLength * 10 + heightTotal / 3; // - 3 just a coonstant
// scales
var scaleX = d3.scaleLinear()
.domain([timeBegin, timeEnd])
.range([0, widthSVG]);
var scaleY = d3.scaleLinear()
.domain([0, laneLength])
.range([0, graphHeight]);
var colorScale = d3.scaleOrdinal(d3.schemeCategory10);
var chart = d3.select("body")
.append("svg")
.attr("width", widthSVG + margin.right + margin.left)
.attr("height", heightSVG + margin.top + margin.bottom)
.attr("class", "chart-table");
var graph = chart.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")")
.attr("width", widthSVG)
.attr("height", graphHeight)
.attr("class", "graph-square");
// Draw the axis
chart.append("g")
.attr("transform", "translate(" + margin.left + ", " + (graphHeight + 20) + ")") // This controls the vertical position of the Axis
.call(d3.axisBottom(scaleX));
// Delimitation lines
graph.append("g").selectAll(".laneLines")
.data(timeSegments)
.enter().append("line")
.attr("x1", 0)
.attr("y1", function(d) {
return scaleY(d.lane);
})
.attr("x2", widthSVG)
.attr("y2", function(d) {
return scaleY(d.lane);
})
.attr("stroke", "lightgray");
// Lanes Names display
graph.append("g").selectAll(".laneText")
.data(lanes)
.enter().append("text")
.text(function(d) {
return d;
})
.attr("x", -margin.right)
.attr("y", function(d, i) {
return scaleY(i + .5);
})
.attr("dy", ".5ex")
.attr("text-anchor", "end")
.attr("class", "laneText");
// Add DIV for "hover_info"
var div = d3.select("body").append("div")
.attr("class", "tooltip-donut")
.style("opacity", 0);
// Graph item rects
graph.append("g").selectAll(".graphItem")
.data(timeSegments)
.enter().append("rect")
.attr("x", function(d) {
return scaleX(d.segment_start);
})
.attr("y", function(d) {
let shiftVertical = 9;
if (d.flag) {
shiftVertical = 0
};
return scaleY(d.lane + .5) - shiftVertical;
})
.attr("width", function(d) {
return scaleX(d.segment_end - d.segment_start + scaleX.domain()[0]);
})
.attr("height", 10)
.style("fill", function(d) {
return colorScale(d.lane);
})
// Hover effect
.on('mouseover', function(d, i) {
d3.select(this).transition()
.duration('50')
.attr('opacity', '.5');
div.transition()
.duration(50)
.style("opacity", 1);
let hover_info = ("id:" + d.id + "<br/>" + "start:" + d.segment_start + "<br/>" + "end:" + d.segment_end).toString();
//Makes the new div appear on hover:
div.html(hover_info)
.style("left", (d3.event.pageX + 10) + "px")
.style("top", (d3.event.pageY - 15) + "px");
})
.on('mouseout', function(d, i) {
d3.select(this).transition()
.duration('50')
.attr('opacity', '1')
//Makes the new div disappear:
div.transition()
.duration('50')
.style("opacity", 0);
});
</script>
</body>
</html>
I want the nodes to have labels, and the lines to be arrows pointing to the edge of the nodes. And I also want the weight to be on the edges. I am new to D3 and having troubles finding examples to do so. Most of the example graphs are force directed, or creating a directing graph. I wanted to make a kind of a path diagram that is NOT interactive at all.
Basically, I want the source node to point to the target nodes. I just want to draw this graph in d3. I feel like this is really simple, but I just can't seem to figure it out. Any suggestions?
<div id="graph">
<script>
var vis = d3.select("#graph")
.append("svg")
.attr("width", 1000)
.attr("height", 1000);
var nodes = [
{label: "Social Dominance", x: 300, y:400},
{label: "Gender Identification", x: 500, y: 200},
{label: "Hostile Sexism", x:500, y:600},
{label: "Collactive Action", x:700, y:400}
],
edges =[
{source: nodes[0], target: nodes[1], weight: 0},
{source: nodes[0], target: nodes[2], weight: 0},
{source: nodes[0], target: nodes[3], weight: 0},
{source: nodes[1], target: nodes[3], weidht: 0},
{source: nodes[2], target: nodes[3], weight: 0}
];
vis.selectAll("circle.nodes")
.data(nodes)
.enter()
.append("svg:circle")
.attr("cx", function(d) { return d.x; })
.attr("cy", function(d) { return d.y; })
.attr("r", "60px")
.attr("fill", "pink");
vis.selectAll("line")
.data(edges)
.enter()
.append("line")
.attr("id", function(d,i){return 'edge'})
.attr('marker-end', 'url(#arrowhead)')
.style("stroke", "#ccc");
vis.selectAll(".nodelabel")
.data(nodes)
.enter()
.append("text")
.attr({"cx":function(d){return d.x;},
"cy":function(d){return d.y;},
"class":"nodelabel",
"stroke":"black"})
.text(function(d){return d.name;});
vis.selectAll(".edgepath")
.data(edges)
.enter()
.append('path')
.attr({'d': function(d) {return 'M '+d.source.x+' '+d.source.y+' L '+ d.target.x +' '+d.target.y},
'class':'edgepath',
'fill-opacity':100,
'stroke-opacity':100,
'fill':'blue',
'stroke':'black',
'id':function(d,i) {return 'edgepath'+i}});
vis.append('defs').append('marker')
.attr({'id':'arrowhead',
'viewBox':'-0 -5 10 10',
'refX':25,
'refY':0,
//'markerUnits':'strokeWidth',
'orient':'auto',
'markerWidth':100,
'markerHeight':100,
'xoverflow':'visible'})
.append('svg:path')
.attr('d', 'M 0,-5 L 10 ,0 L 0,5')
.attr('fill', '#ccc')
.attr('stroke','#ccc');
</script>
</div>
You need to make some changes in your code. Here is a summary:
Texts don't have cx or cy attributes. They should be x and y;
The text property is label, not name;
You have to set the markers to the <path> elements, not to the <line> ones;
Change the marker attributes to better fit the edge of the circles.
Here is your code with those changes:
<script src="https://d3js.org/d3.v3.min.js"></script>
<div id="graph">
<script>
var vis = d3.select("#graph")
.append("svg")
.attr("width", 1000)
.attr("height", 1000);
var nodes = [{
label: "Social Dominance",
x: 300,
y: 400
}, {
label: "Gender Identification",
x: 500,
y: 200
}, {
label: "Hostile Sexism",
x: 500,
y: 600
}, {
label: "Collactive Action",
x: 700,
y: 400
}],
edges = [{
source: nodes[0],
target: nodes[1],
weight: 0
}, {
source: nodes[0],
target: nodes[2],
weight: 0
}, {
source: nodes[0],
target: nodes[3],
weight: 0
}, {
source: nodes[1],
target: nodes[3],
weidht: 0
}, {
source: nodes[2],
target: nodes[3],
weight: 0
}];
vis.selectAll(".edgepath")
.data(edges)
.enter()
.append('path')
.attr({
'd': function(d) {
return 'M ' + d.source.x + ' ' + d.source.y + ' L ' + d.target.x + ' ' + d.target.y
},
'class': 'edgepath',
'fill-opacity': 100,
'stroke-opacity': 100,
'fill': 'blue',
'stroke': 'black',
'marker-end': 'url(#arrowhead)',
'id': function(d, i) {
return 'edgepath' + i
}
});
vis.selectAll("circle.nodes")
.data(nodes)
.enter()
.append("svg:circle")
.attr("cx", function(d) {
return d.x;
})
.attr("cy", function(d) {
return d.y;
})
.attr("r", "60px")
.attr("fill", "pink");
vis.selectAll(".nodelabel")
.data(nodes)
.enter()
.append("text")
.attr({
"x": function(d) {
return d.x;
},
"y": function(d) {
return d.y;
},
"class": "nodelabel",
"text-anchor": "middle",
"fill": "black"
})
.text(function(d) {
return d.label;
});
vis.append('defs').append('marker')
.attr({
'id': 'arrowhead',
'viewBox': '-0 -5 10 10',
'refX': 70,
'refY': 0,
//'markerUnits':'strokeWidth',
'orient': 'auto',
'markerWidth': 10,
'markerHeight': 10,
'xoverflow': 'visible'
})
.append('svg:path')
.attr('d', 'M 0,-5 L 10 ,0 L 0,5')
.attr('fill', '#aaa')
.attr('stroke', '#aaa');
</script>
</div>
I have created five node using d3.js, and make links each other to make a polygon but they are not adjacent position to make a polygon, instead its making a random view other than a polygon.Am I missing something here, please take a look and suggest me.
var width = 300,
height = 300
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height);
var force = d3.layout.force()
.gravity(1)
.linkDistance(200)
.charge(-100)
.size([width, height]);
var datajson = {
"nodes": [{
"name": "a",
"group": 2
}, {
"name": "b",
"group": 1
}, {
"name": "c",
"group": 1
}, {
"name": "d",
"group": 2
}, {
"name": "e",
"group": 2
}],
"links": [{
"source": 0,
"target": 1,
"value": 1,
"distance": 90
}, {
"source": 1,
"target": 2,
"value": 2,
"distance": 90
}, {
"source": 2,
"target": 3,
"value": 3,
"distance": 90
}, {
"source": 3,
"target": 4,
"value": 5,
"distance": 90
}, {
"source": 4,
"target": 0,
"value": 5,
"distance": 90
}]
}
force
.nodes(datajson.nodes)
.links(datajson.links)
.start();
var drag = force.drag()
.on("dragstart", dblclick);
var link = svg.selectAll(".link")
.data(datajson.links)
.enter().append("line")
.attr("class", "link");
var node = svg.selectAll(".node")
.data(datajson.nodes)
.enter().append("g")
.attr("class", "node")
.call(force.drag);
node.append("image")
.attr("x", -8)
.attr("y", -8)
.attr("width", 45)
.attr("height", 45)
.attr("xlink:href", function(d) {
var rnd = Math.floor(Math.random() * 64 + 1);
return null;
});
node.append("text")
.attr("dx", 12)
.attr("dy", ".35em")
.text(function(d) {
return d.name
});
force.on("tick", function() {
link.attr("x1", function(d) {
return d.source.x;
})
.attr("y1", function(d) {
return d.source.y;
})
.attr("x2", function(d) {
return d.target.x;
})
.attr("y2", function(d) {
return d.target.y;
});
node.attr("transform", function(d) {
return "translate(" + d.x + "," + d.y + ")";
});
});
function dblclick(d) {
d3.select(this).classed("fixed", d.px = d.x, d.py = d.y);
console.log(d);
}
.link {
stroke: #dfdfdf;
}
.node text {
pointer-events: none;
font: 10px sans-serif;
}
.link.red {
stroke: blue;
}
<!DOCTYPE html>
<meta charset="utf-8">
<body>
<script src="http://d3js.org/d3.v3.min.js"></script>
</body>
When you call
force
.nodes(datajson.nodes)
.links(datajson.links)
.start();
d3 randomly picks starting positions for the nodes, because they don't yet have x and y properties assigned.
However, prior to calling the code above, you could loop over each node and assign it x and y of the corners of the polygon, and they should maintain that relationship. They might not bounce around much though, because they'd already be at their intended position. In that case, you can slightly vary their position relative to their final intended position, by adding some random x and y displacement to the starting values.
Working example
The code that pre-positions the nodes is
var numNodes = datajson.nodes.length
var r = 20;
datajson.nodes.forEach(function(node, i) {
node.x = width/2 + r * Math.sin(2 * Math.PI * i / numNodes)
node.y = height/2 + r * Math.cos(2 * Math.PI * i / numNodes)
console.log(node)
})
I also had to tweak charge (-1000) and linkDistance (100) to make it work.
var width = 300,
height = 300
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height);
var force = d3.layout.force()
.gravity(.5)
.linkDistance(100)
.charge(-1000)
.size([width, height]);
var datajson = {
"nodes": [{
"name": "a",
"group": 2
}, {
"name": "b",
"group": 1
}, {
"name": "c",
"group": 1
}, {
"name": "d",
"group": 2
}, {
"name": "e",
"group": 2
}],
"links": [{
"source": 0,
"target": 1,
"value": 1,
"distance": 90
}, {
"source": 1,
"target": 2,
"value": 2,
"distance": 90
}, {
"source": 2,
"target": 3,
"value": 3,
"distance": 90
}, {
"source": 3,
"target": 4,
"value": 5,
"distance": 90
}, {
"source": 4,
"target": 0,
"value": 5,
"distance": 90
}]
}
var numNodes = datajson.nodes.length
var r = 20;
datajson.nodes.forEach(function(node, i) {
node.x = width/2 + r * Math.sin(2 * Math.PI * i / numNodes)
node.y = height/2 + r * Math.cos(2 * Math.PI * i / numNodes)
console.log(node)
})
force
.nodes(datajson.nodes)
.links(datajson.links)
.start();
var drag = force.drag()
.on("dragstart", dblclick);
var link = svg.selectAll(".link")
.data(datajson.links)
.enter().append("line")
.attr("class", "link");
var node = svg.selectAll(".node")
.data(datajson.nodes)
.enter().append("g")
.attr("class", "node")
.call(force.drag);
node.append("image")
.attr("x", -8)
.attr("y", -8)
.attr("width", 45)
.attr("height", 45)
.attr("xlink:href", function(d) {
var rnd = Math.floor(Math.random() * 64 + 1);
return null;
});
node.append("text")
.attr("dx", 12)
.attr("dy", ".35em")
.text(function(d) {
return d.name
});
force.on("tick", function() {
link.attr("x1", function(d) {
return d.source.x;
})
.attr("y1", function(d) {
return d.source.y;
})
.attr("x2", function(d) {
return d.target.x;
})
.attr("y2", function(d) {
return d.target.y;
});
node.attr("transform", function(d) {
return "translate(" + d.x + "," + d.y + ")";
});
});
function dblclick(d) {
d3.select(this).classed("fixed", d.px = d.x, d.py = d.y);
console.log(d);
}
.link {
stroke: #dfdfdf;
}
.node text {
pointer-events: none;
font: 10px sans-serif;
}
.link.red {
stroke: blue;
}
<!DOCTYPE html>
<meta charset="utf-8">
<body>
<script src="http://d3js.org/d3.v3.min.js"></script>
</body>
I've seen this answer and this too, but they don't work.
My code is on Fiddle.
Two questions:
1. On clicking a node and pressing the delete button on the keyboard the node and corresponding links get deleted, but why am I not able to drag the remaining nodes afterward?
2. I tried attaching an image (using the path in the nodes array), but the image doesn't appear. The circles just disappear and no image appears (the path to the image is correct. In the same program I tried displaying the image at a corner of the screen and it worked).
The code:
<!DOCTYPE html>
<meta charset="utf-8">
<style>
.background { /*stroke: gray; stroke-width: 1px; fill: rgb(252,231,216);*/ cursor: move; }
.link { stroke: #000; stroke-width: 1.5px; }
.node { fill: #ccc; /*stroke: #000;*/ stroke-width: 1.5px; cursor: pointer;}
.node.fixed { fill: #f00; cursor: pointer;}
text { font: 10px sans-serif; pointer-events: none; text-shadow: 0 1px 0 #fff, 1px 0 0 #fff, 0 -1px 0 #fff, -1px 0 0 #fff; }
</style>
<body>
<script src="d3/d3.v3.js"></script>
<div id="topologyArea"></div>
<script>
var nodes = [//it's not necessary to give x and y values to nodes. One gets created for every empty object you insert here like this {}
{id: 1, x: 470, y: 410, icon: "images/abc.jpg"},
{id: 2, x: 493, y: 364, icon: "images/abc.jpg"},
{id: 3, x: 442, y: 365, icon: "images/abc.jpg"},
{id: 4, x: 467, y: 314, icon: "images/abc.jpg"},
{id: 5, x: 477, y: 248, icon: "images/fsd.jpg"},
{id: 6, x: 425, y: 207, icon: "images/sdfs.jpg"},
{id: 7, x: 402, y: 155, icon: "images/dfs.jpg"},
{id: 8, x: 369, y: 196, icon: "images/abc.jpg"},
{id: 9, x: 350, y: 148, icon: "images/abc.jpg"},
{id: 10, x: 539, y: 222, icon: "images/abc.jpg"},
{id: 11, x: 594, y: 235, icon: "images/abc.jpg"},
{id: 12, x: 582, y: 185, icon: "images/abc.jpg"},
{id: 13, x: 633, y: 200, icon: "images/abc.jpg"}
];
var links = [
{id: 1, source: 0, target: 1},
{id: 2, source: 1, target: 2},
{id: 3, source: 0, target: 2},
{id: 4, source: 1, target: 3},
{id: 5, source: 3, target: 2},
{id: 6, source: 3, target: 4},
{id: 7, source: 4, target: 5},
{id: 8, source: 5, target: 6},
{id: 9, source: 5, target: 7},
{id: 10, source: 6, target: 7},
{id: 11, source: 6, target: 8},
{id: 12, source: 7, target: 8},
{id: 13, source: 9, target: 4},
{id: 14, source: 9, target: 11},
{id: 15, source: 9, target: 10},
{id: 16, source: 10, target: 11},
{id: 17, source: 11, target: 12},
{id: 18, source: 12, target: 10}
];
var margin = {top: -5, right: -5, bottom: -5, left: -5}, width = 960 - margin.left - margin.right, height = 500 - margin.top - margin.bottom;
var iconOffset = -10, iconSize = 20;
var mousedown_node = null, mouseup_node = null, mousedown_link = null;
var nodeDeletionActivated = false;
var zoom = d3.behavior.zoom().scaleExtent([0.2, 2]).on("zoom", zoomed);
var svg = d3.select("#topologyArea").append("svg").attr("width", width + margin.left + margin.right).attr("height", height + margin.top + margin.bottom).attr('class', 'background').attr("transform", "translate(" + margin.left + "," + margin.right + ")");
var rect = svg.append("rect").attr("fill","transparent").attr("width", width + margin.left + margin.right).attr("height", height + margin.top + margin.bottom)
.on("mousedown", mousedownOnBackground);
rect.call(zoom);
var elementHolderLayer = svg.append("g");;
var linkLayer, nodeLayer;
d3.select(window).on("keydown", keydown);// add keyboard callback
redraw(elementHolderLayer);
function redraw(theLayer)//after updating the nodes and links arrays, use this function to re-draw the force graph
{
var force = d3.layout.force().size([width, height]).charge(-400).linkDistance(40).on("tick", tick);
var dragElement = force.drag().on("dragstart", dragstarted);
linkLayer = null; nodeLayer = null;
linkLayer = theLayer.selectAll(".link");
nodeLayer = theLayer.selectAll(".node");
linkLayer = linkLayer.data(links, function(d) {return d.id; }).exit().remove();
linkLayer = theLayer.selectAll(".link").data(links, function(d) {return d.id; }).enter().append("line").attr("class", "link");
nodeLayer = nodeLayer.data(nodes, function(d) {return d.id; }).exit().remove();
nodeLayer = theLayer.selectAll(".node").data(nodes, function(d) {return d.id; }).enter().append("circle").attr("class", "node").attr("r", 12)
.on("dblclick", dblclick).style("fill", function(d,i) { return d3.rgb(i*15, i*15, i*15); })
.on("mouseup", function(d,i) { mouseup(d,i);})
.on("mousemove", function(d,i) {mousemove(d,i);})
.on("mousedown", function(d,i) {mousedown(d,i);})
.call(dragElement)
//.classed("dragging", true)
.classed("fixed", function(d) {d.fixed = true;});
force.nodes(nodes).links(links).start();
}//redraw
function tick()
{
linkLayer.attr("x1", function(d) { return d.source.x; })
.attr("y1", function(d) { return d.source.y; })
.attr("x2", function(d) { return d.target.x; })
.attr("y2", function(d) { return d.target.y; });
nodeLayer.attr("cx", function(d) {return d.x; }).attr("cy", function(d) { return d.y; });
}
function dblclick(d) { d3.select(this).classed("fixed", d.fixed = false); }
function dragstarted(d)
{
console.log("dragstarted for "+this);
//d3.event.sourceEvent.stopPropagation();
//d3.select(this).classed("dragging", true);
//d3.select(this).classed("fixed", d.fixed = true);
}
function zoomed() { elementHolderLayer.attr("transform", "translate("+d3.event.translate+")scale(" + d3.event.scale + ")"); }
function spliceLinksForNode(node) //remove the links attached to a node that got deleted
{
toSplice = links.filter(function(l) { return (l.source === node) || (l.target === node); });
toSplice.map(function(l) {links.splice(links.indexOf(l), 1); });
}
function keydown()
{
//if (!selected_node && !selected_link) return;
switch (d3.event.keyCode)
{
case 8:
{// backspace
}
case 46:
{ // delete
if (mousedown_node)
{
selected_node = mousedown_node;
if (selected_node)
{
nodes.splice(nodes.indexOf(selected_node), 1);
spliceLinksForNode(selected_node);
}
else if (selected_link) { links.splice(links.indexOf(selected_link), 1); }
selected_link = null;
selected_node = null;
redraw(elementHolderLayer);
}
break;
}
}
}//keydown
function mousedown(d,i) { mousedown_node = d; console.log("mousedown"); }
function mousedownOnBackground() {resetMouseVars();}
function mousemove(d, i) {console.log("mousemove");}
function mouseup(d, i) {console.log("mouseup");}
function resetMouseVars()
{
mousedown_node = null;
mouseup_node = null;
mousedown_link = null;
}
</script>
There is one problem in the redraw function in your code.
linkLayer = linkLayer.data(links, function(d) {return d.id; })
.exit()
.remove();
Above line has no use in your code since you are assigning the same variable with links having the old data again.
linkLayer = theLayer.selectAll(".link").data(links, function(d) { return d.id; })
.enter()
.append("line")
.attr("class", "link");
Same happens for nodes. Change your code as shown below.
//Creating links
linkLayer = theLayer.selectAll(".link").data(links, function(d) {
return d.id;
});
linkLayer.enter().append("line").attr("class", "link");
linkLayer.exit().remove();
//Creating Nodes with image icons
var gNodes = nodeLayer.enter().append("g")
.attr("class", "node")
.on("dblclick", dblclick).style("fill", function(d, i) {
return d3.rgb(i * 15, i * 15, i * 15);
})
.on("mouseup", mouseup)
.on("mousemove", mousemove)
.on("mousedown", mousedown)
.call(dragElement)
.classed("fixed", function(d) {
d.fixed = true;
});
gNodes.append("circle")
.attr("r", 12);
gNodes.append("svg:image")
.attr("class", "circle")
.attr("xlink:href",function(d){ return d.icon })
.attr("x", "-8px")
.attr("y", "-8px")
.attr("width", "16px")
.attr("height", "16px");
nodeLayer.exit().remove();
For updating position of circles and images easily, I have grouped them using g elements. So you will need to update the position of g element using transform attribute instead of updating cx and cy attributes of circle. Now, tick function will look like this. Updated fiddle
function tick() {
linkLayer.attr("x1", function(d) { return d.source.x; })
.attr("y1", function(d) { return d.source.y; })
.attr("x2", function(d) { return d.target.x; })
.attr("y2", function(d) { return d.target.y; });
nodeLayer.attr("transform", function(d) {return "translate("+d.x+","+d.y+")"; });
}