Related
I struggle to properly redraw the provided D3 forced graph. As soon as I delete a node, which is NOT the last node, the binding/assignment between link and node is broken. The belonged linktext is either wrong or written on top of an existing.
What I am doing?
On node click I retrieve the node id, which I use to retrieve the correct link array index, by comparing them the source.id and to find the correct node array index position too. Further I use the results to finally splice the link and node array at the correct position.
So whats my problem?
For example I delete node number 3, which should delete the node array object at position 2, which is true. Further the link between 3 --- 1 should be removed from the link array object at position 1 as well, which is also true.
At the end I restart the data assignment and call the restart() function. Which should use the modified nodes and links array´s to merge and redraw the graph. It actually does redraw but the link text is wrong.. instead of node 3 node 5 was deleted.
Help.
var data = {
"nodes": [{
"id": 1
},
{
"id": 2,
},
{
"id": 3,
},
{
"id": 4,
},
{
"id": 5,
}
],
"links": [{
"source": 2,
"target": 1,
"text": "2 --- 1"
},
{
"source": 3,
"target": 1,
"text": "3 --- 1"
},
{
"source": 4,
"target": 1,
"text": "4 --- 1"
},
{
"source": 5,
"target": 1,
"text": "5 --- 1"
}
]
};
let nodes = data.nodes
let links = data.links
//Helper
let nodeToDelete
var width = window.innerWidth,
height = window.innerHeight;
var buttons = d3.select("body").selectAll("button")
.data(["add node", "remove node"])
.enter()
.append("button")
.attr("class", "buttons")
.text(function (d) {
return d;
})
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height)
.call(d3.zoom().on("zoom", function (event) {
svg.attr("transform", event.transform)
}))
.append("g")
var simulation = d3.forceSimulation()
.force("size", d3.forceCenter(width / 2, height / 2))
.force("charge", d3.forceManyBody().strength(-5000))
.force("link", d3.forceLink().id(function (d) {
return d.id
}).distance(250))
linksContainer = svg.append("g").attr("class", "linkscontainer")
nodesContainer = svg.append("g").attr("class", "nodesContainer")
console.log("links_on_init", links)
console.log("nodes_on_init", nodes)
restart()
simulation
.nodes(nodes)
.on("tick", tick)
simulation
.force("link").links(links)
function tick() {
linkLine.attr("d", function (d) {
var dx = (d.target.x - d.source.x),
dy = (d.target.y - d.source.y),
dr = Math.sqrt(dx * dx + dy * dy)
return "M" + d.source.x + "," + d.source.y + "A" + dr + "," + dr + " 0 0,1 " + d.target.x + "," + d.target.y;
})
node
.attr("transform", d => `translate(${d.x}, ${d.y})`);
}
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;
}
buttons.on("click", function (_, d) {
if (d === "add node") {
const newObj = { "id": nodes.length + 1,}
const newLink = {"source": nodes.length + 1, "target": 1, "text": nodes.length + 1 + " --- " + "1"}
nodes.push(newObj)
links.push(newLink)
} else if (d === "remove node") {
if (nodeToDelete != undefined) {
let linkToDeleteIndex = links.findIndex(obj => obj.source.id === nodeToDelete.id )
let nodeToDeleteIndex = nodes.findIndex(obj => obj.id === nodeToDelete.id)
links.splice(linkToDeleteIndex, 1)
nodes.splice(nodeToDeleteIndex, 1)
console.log("links_after_removal", links)
console.log("nodes_after_removal", nodes)
}
}
restart()
})
function restart() {
// Update linkLines
linkLine = linksContainer.selectAll(".linkPath")
.data(links)
linkLine.exit().remove()
const linkLineEnter = linkLine.enter()
.append("path")
.attr("class", "linkPath")
.attr("stroke", "red")
.attr("fill", "transparent")
.attr("stroke-width", 3)
.attr("id", function (_, i) {
return "path" + i
})
linkLine = linkLineEnter.merge(linkLine)
// Update linkText
linkText = linksContainer.selectAll("linkLabel")
.data(links)
linkText.exit().remove()
const linkTextEnter = linkText.enter()
.append("text")
.attr("dy", -10)
.attr("class", "linkLabel")
.attr("id", function (d, i) { return "linkLabel" + i })
.attr("text-anchor", "middle")
.text("")
linkTextEnter.append("textPath")
.attr("xlink:href", function (_, i) {
return "#path" + i
})
.attr("startOffset", "50%")
.attr("opacity", 0.75)
.attr("cursor", "default")
.attr("class", "linkText")
.attr("color", "black")
.text(function (d) {
return d.text
})
linkText = linkTextEnter.merge(linkText)
// Update nodes
node = nodesContainer.selectAll(".nodes")
.data(nodes)
node.exit().remove()
const nodesEnter = node.enter()
.append("g")
.attr("class", "nodes")
.call(d3.drag()
.on("start", dragStarted)
.on("drag", dragged)
.on("end", dragEnded)
)
nodesEnter.selectAll("circle")
.data(d => [d])
.enter()
.append("circle")
.style("stroke", "blue")
.attr("r", 40)
.on("click", function(_, d) {
d3.selectAll("circle")
.attr("fill", "whitesmoke")
d3.select(this)
.attr("fill", "red")
nodeToDelete = d
})
nodesEnter.append("text")
.attr("dominant-baseline", "central")
.attr("text-anchor", "middle")
.attr("font-size", 20)
.attr("fill", "black")
.attr("pointer-events", "none")
.text(function (d) {
return d.id
})
node = nodesEnter.merge(node)
// Update and restart the simulation.
simulation
.nodes(nodes);
simulation
.force("link")
.links(links)
simulation.restart().alpha(1)
}
.link {
stroke: #000;
stroke-width: 1.5px;
}
.nodes {
fill: whitesmoke;
}
.buttons {
margin: 0 1em 0 0;
}
<!DOCTYPE html>
<html lang="de">
<head>
<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0">
<meta charset="utf-8">
<!-- jQuery -->
<script src="https://code.jquery.com/jquery-3.6.3.js"></script>
<!-- D3 -->
<script src="https://d3js.org/d3.v7.min.js"></script>
<!-- fontawesome stylesheet https://fontawesome.com/ -->
<script src="https://kit.fontawesome.com/98a5e27706.js" crossorigin="anonymous"></script>
</head>
<body>
</body>
</html>
You should provide data keys when you do .data(data). So for example when you provide data to the nodes, you may pass key like this:
node = nodesContainer.selectAll(".nodes")
.data(nodes, node => node.id) // Pass node.id as a key so d3 knows which node is a new and which one is old.
You can read about this here - https://observablehq.com/#dnarvaez27/understanding-enter-exit-merge-key-function
Also you've forgot to place . before class name when you adding text labels to the graph, that is why text was duplicated )
Here fixed example, but since you are using array length as ids, there will be duplicates in ids for nodes and links, it will lead to unexpected results, I consider to use uniq ids rather then indexes.
var data = {
"nodes": [{
"id": 1
},
{
"id": 2,
},
{
"id": 3,
},
{
"id": 4,
},
{
"id": 5,
}
],
"links": [{
"source": 2,
"target": 1,
"text": "2 --- 1"
},
{
"source": 3,
"target": 1,
"text": "3 --- 1"
},
{
"source": 4,
"target": 1,
"text": "4 --- 1"
},
{
"source": 5,
"target": 1,
"text": "5 --- 1"
}
]
};
let nodes = data.nodes
let links = data.links
//Helper
let nodeToDelete
var width = window.innerWidth,
height = window.innerHeight;
var buttons = d3.select("body").selectAll("button")
.data(["add node", "remove node"])
.enter()
.append("button")
.attr("class", "buttons")
.text(function (d) {
return d;
})
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height)
.call(d3.zoom().on("zoom", function (event) {
svg.attr("transform", event.transform)
}))
.append("g")
var simulation = d3.forceSimulation()
.force("size", d3.forceCenter(width / 2, height / 2))
.force("charge", d3.forceManyBody().strength(-5000))
.force("link", d3.forceLink().id(function (d) {
return d.id
}).distance(250))
linksContainer = svg.append("g").attr("class", "linkscontainer")
nodesContainer = svg.append("g").attr("class", "nodesContainer")
console.log("links_on_init", links)
console.log("nodes_on_init", nodes)
restart()
simulation
.nodes(nodes)
.on("tick", tick)
simulation
.force("link").links(links)
function tick() {
linkLine.attr("d", function (d) {
var dx = (d.target.x - d.source.x),
dy = (d.target.y - d.source.y),
dr = Math.sqrt(dx * dx + dy * dy)
return "M" + d.source.x + "," + d.source.y + "A" + dr + "," + dr + " 0 0,1 " + d.target.x + "," + d.target.y;
})
node
.attr("transform", d => `translate(${d.x}, ${d.y})`);
}
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;
}
buttons.on("click", function (_, d) {
if (d === "add node") {
const newObj = { "id": nodes.length + 1,}
const newLink = {"source": nodes.length + 1, "target": 1, "text": nodes.length + 1 + " --- " + "1"}
nodes.push(newObj)
links.push(newLink)
} else if (d === "remove node") {
if (nodeToDelete != undefined) {
let linkToDeleteIndex = links.findIndex(obj => obj.source.id === nodeToDelete.id )
let nodeToDeleteIndex = nodes.findIndex(obj => obj.id === nodeToDelete.id)
links.splice(linkToDeleteIndex, 1)
nodes.splice(nodeToDeleteIndex, 1)
console.log("links_after_removal", links)
console.log("nodes_after_removal", nodes)
}
}
restart()
})
function restart() {
// Update linkLines
linkLine = linksContainer.selectAll(".linkPath")
.data(links, link => link.text) // ADD DATA KEY FOR LINK
linkLine.exit().remove()
const linkLineEnter = linkLine.enter()
.append("path")
.attr("class", "linkPath")
.attr("stroke", "red")
.attr("fill", "transparent")
.attr("stroke-width", 3)
.attr("id", function (_, i) {
return "path" + i
})
linkLine = linkLineEnter.merge(linkLine)
// Update linkText
linkText = linksContainer.selectAll(".linkLabel") // FIXED ClassName
.data(links, link => link.text) // ADD DATA KEY FOR TEXT
linkText.exit().remove()
const linkTextEnter = linkText.enter()
.append("text")
.attr("dy", -10)
.attr("class", "linkLabel")
.attr("id", function (d, i) { return "linkLabel" + i })
.attr("text-anchor", "middle")
.text("")
linkTextEnter.append("textPath")
.attr("xlink:href", function (_, i) {
return "#path" + i
})
.attr("startOffset", "50%")
.attr("opacity", 0.75)
.attr("cursor", "default")
.attr("class", "linkText")
.attr("color", "black")
.text(function (d) {
return d.text
})
linkText = linkTextEnter.merge(linkText)
// Update nodes
node = nodesContainer.selectAll(".nodes")
.data(nodes, node => node.id) // ADD DATA KEY FOR NODE
node.exit().remove()
const nodesEnter = node.enter()
.append("g")
.attr("class", "nodes")
.call(d3.drag()
.on("start", dragStarted)
.on("drag", dragged)
.on("end", dragEnded)
)
nodesEnter.selectAll("circle")
.data(d => [d])
.enter()
.append("circle")
.style("stroke", "blue")
.attr("r", 40)
.on("click", function(_, d) {
d3.selectAll("circle")
.attr("fill", "whitesmoke")
d3.select(this)
.attr("fill", "red")
nodeToDelete = d
})
nodesEnter.append("text")
.attr("dominant-baseline", "central")
.attr("text-anchor", "middle")
.attr("font-size", 20)
.attr("fill", "black")
.attr("pointer-events", "none")
.text(function (d) {
return d.id
})
node = nodesEnter.merge(node)
// Update and restart the simulation.
simulation
.nodes(nodes);
simulation
.force("link")
.links(links)
simulation.restart().alpha(1)
}
.link {
stroke: #000;
stroke-width: 1.5px;
}
.nodes {
fill: whitesmoke;
}
.buttons {
margin: 0 1em 0 0;
}
<!DOCTYPE html>
<html lang="de">
<head>
<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0">
<meta charset="utf-8">
<!-- jQuery -->
<script src="https://code.jquery.com/jquery-3.6.3.js"></script>
<!-- D3 -->
<script src="https://d3js.org/d3.v7.min.js"></script>
<!-- fontawesome stylesheet https://fontawesome.com/ -->
<script src="https://kit.fontawesome.com/98a5e27706.js" crossorigin="anonymous"></script>
</head>
<body>
</body>
</html>
I am trying to draw a multi line chart with D3.js. The chart is drawn properly but the x-axis ticks are not aligned correctly with the data points circle.
Find the example code here: https://jsfiddle.net/gopal31795/qsLd7pc5/9/
I assume that there is some error in the code for creating the dots.
// Creating Dots on line
segment.selectAll("dot")
.data(function(d) {
return d.linedata;
})
.enter().append("circle")
.attr("r", 5)
.attr("cx", function(d) {
//return x(parseDate(new Date(d.mins))) + x.rangeBand() / 2;
return x(d.mins);
})
.attr("cy", function(d) {
return y(d.value);
})
.style("stroke", "white")
.style("fill", function(d) {
return color(this.parentNode.__data__.name);
})
.on("mouseenter", function(d) {
d3.select(this).transition().style("opacity", "0.25");
tooltip.html("<span style='color:" + color(this.parentNode.__data__.name) + ";'>" + this.parentNode.__data__.name + "</span>: " + (d.value + "s")).style("visibility", "visible").style("top", (event.pageY + 10) + "px").style("left", (event.pageX) + "px");
})
.on("mouseleave", function(d) {
d3.select(this).transition().style("opacity", "1");
tooltip.style("visibility", "hidden");
});
The problem in your code (which uses the old v3) is that you're using rangeRoundBands. That would be the correct choice if you had a bar chart, for instance.
However, since you're dealing with data points, you should use rangePoints or rangeRoundPoints, which:
Sets the output range from the specified continuous interval. The array interval contains two elements representing the minimum and maximum numeric value. This interval is subdivided into n evenly-spaced points, where n is the number of (unique) values in the input domain.
So, it should be:
var x = d3.scale.ordinal()
.rangeRoundPoints([0, width]);
Here is your code with that change:
var Data = [{
"name": "R",
"linedata": [{
"mins": 0,
"value": 1120
},
{
"mins": 2,
"value": 1040
},
{
"mins": 4,
"value": 1400
},
{
"mins": 6,
"value": 1500
}
]
},
{
"name": "E",
"linedata": [{
"mins": 0,
"value": 1220
},
{
"mins": 2,
"value": 1500
},
{
"mins": 4,
"value": 1610
},
{
"mins": 6,
"value": 1700
}
]
}
];
var margin = {
top: 20,
right: 90,
bottom: 35,
left: 90
},
width = $("#lineChart").width() - margin.left - margin.right,
height = $("#lineChart").width() * 0.3745 - margin.top - margin.bottom;
//var parseDate = d3.time.format("%d-%b");
var x = d3.scale.ordinal()
.rangeRoundPoints([0, width]);
var y = d3.scale.linear()
.range([height, 0]);
var color = d3.scale.category10();
var xAxis = d3.svg.axis()
.scale(x)
.orient("bottom");
var yAxis = d3.svg.axis()
.scale(y)
.orient("left")
.ticks(5);
var xData = Data[0].linedata.map(function(d) {
return d.mins;
});
var line = d3.svg.line()
.interpolate("linear")
.x(function(d) {
return x(d.mins);
})
.y(function(d) {
return y(d.value);
});
function transition(path) {
path.transition()
.duration(4000)
.attrTween("stroke-dasharray", tweenDash);
}
function tweenDash() {
var l = this.getTotalLength(),
i = d3.interpolateString("0," + l, l + "," + l);
return function(t) {
return i(t);
};
}
var svg = d3.select("#lineChart").append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
color.domain(Data.map(function(d) {
return d.name;
}));
x.domain(xData);
var valueMax = d3.max(Data, function(r) {
return d3.max(r.linedata, function(d) {
return d.value;
})
});
var valueMin = d3.min(Data, function(r) {
return d3.min(r.linedata, function(d) {
return d.value;
})
});
y.domain([valueMin, valueMax]);
//Drawing X Axis
svg.append("g")
.attr("class", "x-axis")
.attr("transform", "translate(0," + height + ")")
.call(xAxis)
.append("text")
.attr("class", "xAxisText")
.attr("x", width)
.attr("dy", "-.41em")
.style("text-anchor", "end")
.style("fill", "white")
.text("Time(m)");
svg.append("g")
.attr("class", "y-axis")
.call(yAxis)
.append("text")
.attr("transform", "rotate(-90)")
.attr("y", 6)
.attr("dy", ".71em")
.style("text-anchor", "end")
.style("fill", "none")
.style("stroke", "white")
.style("stroke-width", 0.8)
.text("Duration(s)");
// Drawing Lines for each segments
var segment = svg.selectAll(".segment")
.data(Data)
.enter().append("g")
.attr("class", "segment");
segment.append("path")
.attr("class", "line")
.attr("id", function(d) {
return d.name;
})
.attr("visible", 1)
.call(transition)
//.delay(750)
//.duration(1000)
//.ease('linear')
.attr("d", function(d) {
return line(d.linedata);
})
.style("stroke", function(d) {
return color(d.name);
});
// Creating Dots on line
segment.selectAll("dot")
.data(function(d) {
return d.linedata;
})
.enter().append("circle")
.attr("r", 5)
.attr("cx", function(d) {
//return x(parseDate(new Date(d.mins))) + x.rangeBand() / 2;
return x(d.mins);
})
.attr("cy", function(d) {
return y(d.value);
})
.style("stroke", "white")
.style("fill", function(d) {
return color(this.parentNode.__data__.name);
})
.on("mouseenter", function(d) {
d3.select(this).transition().style("opacity", "0.25");
tooltip.html("<span style='color:" + color(this.parentNode.__data__.name) + ";'>" + this.parentNode.__data__.name + "</span>: " + (d.value + "s")).style("visibility", "visible").style("top", (event.pageY + 10) + "px").style("left", (event.pageX) + "px");
})
.on("mouseleave", function(d) {
d3.select(this).transition().style("opacity", "1");
tooltip.style("visibility", "hidden");
});
path {
fill: none;
stroke: black;
}
line {
stroke: black;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.17/d3.min.js"></script>
<div class="card-body" id="lineChart">
</div>
Finally, as a tip: don't mix D3 and jQuery.
This is a 2nd question that builds off of this a previous question of mine here - D3 Force Graph With Arrows and Curved Edges - shorten links so arrow doesnt overlap nodes - on how to shorten curved links for a d3 force graph.
My latest struggle involves centering the text placed on top of the links, actually over the links. Here is a reproducible example showing my issue (apologies for the long code. a lot was needed to create a reproducible example, although I am only working on a small bit of it currently):
const svg = d3.select('#mySVG')
const nodesG = svg.select("g.nodes")
const linksG = svg.select("g.links")
var graphs = {
"nodes": [{
"name": "Peter",
"label": "Person",
"id": 1
},
{
"name": "Michael",
"label": "Person",
"id": 2
},
{
"name": "Neo4j",
"label": "Database",
"id": 3
},
{
"name": "Graph Database",
"label": "Database",
"id": 4
}
],
"links": [{
"source": 1,
"target": 2,
"type": "KNOWS",
"since": 2010
},
{
"source": 1,
"target": 3,
"type": "FOUNDED"
},
{
"source": 2,
"target": 3,
"type": "WORKS_ON"
},
{
"source": 3,
"target": 4,
"type": "IS_A"
}
]
}
svg.append('defs').append('marker')
.attr('id', 'arrowhead')
.attr('viewBox', '-0 -5 10 10')
.attr('refX', 0)
.attr('refY', 0)
.attr('orient', 'auto')
.attr('markerWidth', 13)
.attr('markerHeight', 13)
.attr('xoverflow', 'visible')
.append('svg:path')
.attr('d', 'M 0,-5 L 10 ,0 L 0,5')
.attr('fill', '#999')
.style('stroke', 'none');
const simulation = d3.forceSimulation()
.force("link", d3.forceLink().id(d => d.id))
.force("charge", d3.forceManyBody())
.force("center", d3.forceCenter(100, 100));
let linksData = graphs.links.map(link => {
var obj = link;
obj.source = link.source;
obj.target = link.target;
return obj;
})
const links = linksG
.selectAll("g")
.data(graphs.links)
.enter().append("g")
.attr("cursor", "pointer")
const linkLines = links
.append("path")
.attr('stroke', '#000000')
.attr('opacity', 0.75)
.attr("stroke-width", 1)
.attr("fill", "transparent")
.attr('marker-end', 'url(#arrowhead)');
const linkText = links
.append("text")
.attr("x", d => (d.source.x + (d.target.x - d.source.x) * 0.5))
.attr("y", d => (d.source.y + (d.target.y - d.source.y) * 0.5))
.attr('stroke', '#000000')
.attr("text-anchor", "middle")
.attr('opacity', 1)
.text((d,i) => `${i}`);
const nodes = nodesG
.selectAll("g")
.data(graphs.nodes)
.enter().append("g")
.attr("cursor", "pointer")
.call(d3.drag()
.on("start", dragstarted)
.on("drag", dragged)
.on("end", dragended));
const circles = nodes.append("circle")
.attr("r", 12)
.attr("fill", "000000")
nodes.append("title")
.text(function(d) {
return d.id;
});
simulation
.nodes(graphs.nodes)
.on("tick", ticked);
simulation.force("link", d3.forceLink().links(linksData)
.id((d, i) => d.id)
.distance(150));
function ticked() {
linkLines.attr("d", function(d) {
var dx = (d.target.x - d.source.x),
dy = (d.target.y - d.source.y),
dr = Math.sqrt(dx * dx + dy * dy);
return "M" + d.source.x + "," + d.source.y + "A" + dr + "," + dr + " 0 0,1 " + d.target.x + "," + d.target.y;
});
// recalculate and back off the distance
linkLines.attr("d", function(d) {
// length of current path
var pl = this.getTotalLength(),
// radius of circle plus backoff
r = (12) + 30,
// position close to where path intercepts circle
m = this.getPointAtLength(pl - r);
var dx = m.x - d.source.x,
dy = m.y - d.source.y,
dr = Math.sqrt(dx * dx + dy * dy);
return "M" + d.source.x + "," + d.source.y + "A" + dr + "," + dr + " 0 0,1 " + m.x + "," + m.y;
});
linkText
.attr("x", function(d) { return (d.source.x + (d.target.x - d.source.x) * 0.5); })
.attr("y", function(d) { return (d.source.y + (d.target.y - d.source.y) * 0.5); })
nodes
.attr("transform", d => `translate(${d.x}, ${d.y})`);
}
function dragstarted(d) {
if (!d3.event.active) simulation.alphaTarget(0.3).restart();
d.fx = d.x;
d.fy = d.y;
}
function dragged(d) {
d.fx = d3.event.x;
d.fy = d3.event.y;
}
function dragended(d) {
if (!d3.event.active) simulation.alphaTarget(0);
d.fx = null;
d.fy = null;
}
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<script src="//d3js.org/d3.v4.min.js" type="text/javascript"></script>
</head>
<body>
<svg id="mySVG" width="500" height="500">
<g class="links" />
<g class="nodes" />
</svg>
I know that the issue with my code is with the setting of the x and y values for the linkText here:
linkText
.attr("x", function(d) { return (d.source.x + (d.target.x - d.source.x) * 0.5); })
.attr("y", function(d) { return (d.source.y + (d.target.y - d.source.y) * 0.5); })
...and also earlier in my code. I am not sure how to update these functions to account for the fact that the links are curved lines (not straight lines from node to node).
The larger force graph for my project has many more links and nodes, and positioning of the text over the center of the curved linkLines would be preferable.
Any help with this is appreciated!
There are several different ways to fix this. The two most obvious ones are:
Using getPointAtLength() to get the middle of the <path>, and positioning the texts there;
Using a <textPath> element.
In my solution I'll choose #2 mainly because, using a text path, the numbers can flip according the paths' orientation, some of them ending up upside down (I'm assuming that this is what you want).
So, we append the textPaths...
const linkText = links
.append("text")
.attr("dy", -4)
.append("textPath")
.attr("xlink:href", function(_, i) {
return "#path" + i
})
.attr("startOffset", "50%")
.text((d, i) => `${i}`);
... having given the paths unique ids:
.attr("id", function(_, i) {
return "path" + i
})
This is the code with those changes:
const svg = d3.select('#mySVG')
const nodesG = svg.select("g.nodes")
const linksG = svg.select("g.links")
var graphs = {
"nodes": [{
"name": "Peter",
"label": "Person",
"id": 1
},
{
"name": "Michael",
"label": "Person",
"id": 2
},
{
"name": "Neo4j",
"label": "Database",
"id": 3
},
{
"name": "Graph Database",
"label": "Database",
"id": 4
}
],
"links": [{
"source": 1,
"target": 2,
"type": "KNOWS",
"since": 2010
},
{
"source": 1,
"target": 3,
"type": "FOUNDED"
},
{
"source": 2,
"target": 3,
"type": "WORKS_ON"
},
{
"source": 3,
"target": 4,
"type": "IS_A"
}
]
}
svg.append('defs').append('marker')
.attr('id', 'arrowhead')
.attr('viewBox', '-0 -5 10 10')
.attr('refX', 0)
.attr('refY', 0)
.attr('orient', 'auto')
.attr('markerWidth', 13)
.attr('markerHeight', 13)
.attr('xoverflow', 'visible')
.append('svg:path')
.attr('d', 'M 0,-5 L 10 ,0 L 0,5')
.attr('fill', '#999')
.style('stroke', 'none');
const simulation = d3.forceSimulation()
.force("link", d3.forceLink().id(d => d.id))
.force("charge", d3.forceManyBody())
.force("center", d3.forceCenter(100, 100));
let linksData = graphs.links.map(link => {
var obj = link;
obj.source = link.source;
obj.target = link.target;
return obj;
})
const links = linksG
.selectAll("g")
.data(graphs.links)
.enter().append("g")
.attr("cursor", "pointer")
const linkLines = links
.append("path")
.attr("id", function(_, i) {
return "path" + i
})
.attr('stroke', '#000000')
.attr('opacity', 0.75)
.attr("stroke-width", 1)
.attr("fill", "transparent")
.attr('marker-end', 'url(#arrowhead)');
const linkText = links
.append("text")
.attr("dy", -4)
.append("textPath")
.attr("xlink:href", function(_, i) {
return "#path" + i
})
.attr("startOffset", "50%")
.attr('stroke', '#000000')
.attr('opacity', 1)
.text((d, i) => `${i}`);
const nodes = nodesG
.selectAll("g")
.data(graphs.nodes)
.enter().append("g")
.attr("cursor", "pointer")
.call(d3.drag()
.on("start", dragstarted)
.on("drag", dragged)
.on("end", dragended));
const circles = nodes.append("circle")
.attr("r", 12)
.attr("fill", "000000")
nodes.append("title")
.text(function(d) {
return d.id;
});
simulation
.nodes(graphs.nodes)
.on("tick", ticked);
simulation.force("link", d3.forceLink().links(linksData)
.id((d, i) => d.id)
.distance(150));
function ticked() {
linkLines.attr("d", function(d) {
var dx = (d.target.x - d.source.x),
dy = (d.target.y - d.source.y),
dr = Math.sqrt(dx * dx + dy * dy);
return "M" + d.source.x + "," + d.source.y + "A" + dr + "," + dr + " 0 0,1 " + d.target.x + "," + d.target.y;
});
// recalculate and back off the distance
linkLines.attr("d", function(d) {
// length of current path
var pl = this.getTotalLength(),
// radius of circle plus backoff
r = (12) + 30,
// position close to where path intercepts circle
m = this.getPointAtLength(pl - r);
var dx = m.x - d.source.x,
dy = m.y - d.source.y,
dr = Math.sqrt(dx * dx + dy * dy);
return "M" + d.source.x + "," + d.source.y + "A" + dr + "," + dr + " 0 0,1 " + m.x + "," + m.y;
});
linkText
.attr("x", function(d) {
return (d.source.x + (d.target.x - d.source.x) * 0.5);
})
.attr("y", function(d) {
return (d.source.y + (d.target.y - d.source.y) * 0.5);
})
nodes
.attr("transform", d => `translate(${d.x}, ${d.y})`);
}
function dragstarted(d) {
if (!d3.event.active) simulation.alphaTarget(0.3).restart();
d.fx = d.x;
d.fy = d.y;
}
function dragged(d) {
d.fx = d3.event.x;
d.fy = d3.event.y;
}
function dragended(d) {
if (!d3.event.active) simulation.alphaTarget(0);
d.fx = null;
d.fy = null;
}
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<script src="//d3js.org/d3.v4.min.js" type="text/javascript"></script>
</head>
<body>
<svg id="mySVG" width="500" height="500">
<g class="links" />
<g class="nodes" />
</svg>
On the other hand, if you don't want some of the texts upside down, use getPointAtLength() to get the middle of the path, which is the approach #1:
.attr("x", function(d) {
const length = this.previousSibling.getTotalLength();
return this.previousSibling.getPointAtLength(length/2).x
})
.attr("y", function(d) {
const length = this.previousSibling.getTotalLength();
return this.previousSibling.getPointAtLength(length/2).y
})
Here is the demo:
const svg = d3.select('#mySVG')
const nodesG = svg.select("g.nodes")
const linksG = svg.select("g.links")
var graphs = {
"nodes": [{
"name": "Peter",
"label": "Person",
"id": 1
},
{
"name": "Michael",
"label": "Person",
"id": 2
},
{
"name": "Neo4j",
"label": "Database",
"id": 3
},
{
"name": "Graph Database",
"label": "Database",
"id": 4
}
],
"links": [{
"source": 1,
"target": 2,
"type": "KNOWS",
"since": 2010
},
{
"source": 1,
"target": 3,
"type": "FOUNDED"
},
{
"source": 2,
"target": 3,
"type": "WORKS_ON"
},
{
"source": 3,
"target": 4,
"type": "IS_A"
}
]
}
svg.append('defs').append('marker')
.attr('id', 'arrowhead')
.attr('viewBox', '-0 -5 10 10')
.attr('refX', 0)
.attr('refY', 0)
.attr('orient', 'auto')
.attr('markerWidth', 13)
.attr('markerHeight', 13)
.attr('xoverflow', 'visible')
.append('svg:path')
.attr('d', 'M 0,-5 L 10 ,0 L 0,5')
.attr('fill', '#999')
.style('stroke', 'none');
const simulation = d3.forceSimulation()
.force("link", d3.forceLink().id(d => d.id))
.force("charge", d3.forceManyBody())
.force("center", d3.forceCenter(100, 100));
let linksData = graphs.links.map(link => {
var obj = link;
obj.source = link.source;
obj.target = link.target;
return obj;
})
const links = linksG
.selectAll("g")
.data(graphs.links)
.enter().append("g")
.attr("cursor", "pointer")
const linkLines = links
.append("path")
.attr('stroke', '#000000')
.attr('opacity', 0.75)
.attr("stroke-width", 1)
.attr("fill", "transparent")
.attr('marker-end', 'url(#arrowhead)');
const linkText = links
.append("text")
.attr("x", function(d) {
const length = this.previousSibling.getTotalLength();
return this.previousSibling.getPointAtLength(length / 2).x
})
.attr("y", function(d) {
const length = this.previousSibling.getTotalLength();
return this.previousSibling.getPointAtLength(length / 2).y
})
.attr('stroke', '#000000')
.attr("text-anchor", "middle")
.attr("dominant-baseline", "central")
.attr('opacity', 1)
.text((d, i) => `${i}`);
const nodes = nodesG
.selectAll("g")
.data(graphs.nodes)
.enter().append("g")
.attr("cursor", "pointer")
.call(d3.drag()
.on("start", dragstarted)
.on("drag", dragged)
.on("end", dragended));
const circles = nodes.append("circle")
.attr("r", 12)
.attr("fill", "000000")
nodes.append("title")
.text(function(d) {
return d.id;
});
simulation
.nodes(graphs.nodes)
.on("tick", ticked);
simulation.force("link", d3.forceLink().links(linksData)
.id((d, i) => d.id)
.distance(150));
function ticked() {
linkLines.attr("d", function(d) {
var dx = (d.target.x - d.source.x),
dy = (d.target.y - d.source.y),
dr = Math.sqrt(dx * dx + dy * dy);
return "M" + d.source.x + "," + d.source.y + "A" + dr + "," + dr + " 0 0,1 " + d.target.x + "," + d.target.y;
});
// recalculate and back off the distance
linkLines.attr("d", function(d) {
// length of current path
var pl = this.getTotalLength(),
// radius of circle plus backoff
r = (12) + 30,
// position close to where path intercepts circle
m = this.getPointAtLength(pl - r);
var dx = m.x - d.source.x,
dy = m.y - d.source.y,
dr = Math.sqrt(dx * dx + dy * dy);
return "M" + d.source.x + "," + d.source.y + "A" + dr + "," + dr + " 0 0,1 " + m.x + "," + m.y;
});
linkText
.attr("x", function(d) {
const length = this.previousSibling.getTotalLength();
return this.previousSibling.getPointAtLength(length / 2).x
})
.attr("y", function(d) {
const length = this.previousSibling.getTotalLength();
return this.previousSibling.getPointAtLength(length / 2).y
})
nodes
.attr("transform", d => `translate(${d.x}, ${d.y})`);
}
function dragstarted(d) {
if (!d3.event.active) simulation.alphaTarget(0.3).restart();
d.fx = d.x;
d.fy = d.y;
}
function dragged(d) {
d.fx = d3.event.x;
d.fy = d3.event.y;
}
function dragended(d) {
if (!d3.event.active) simulation.alphaTarget(0);
d.fx = null;
d.fy = null;
}
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<script src="//d3js.org/d3.v4.min.js" type="text/javascript"></script>
</head>
<body>
<svg id="mySVG" width="500" height="500">
<g class="links" />
<g class="nodes" />
</svg>
Here I'm assuming that the <path> is the previous sibling of the <text>. If that's not the case in the real code, change this accordingly.
This is a d3 bar chart. line 162 ,
.on("mouseover", console.log("I am on it"))
should only happen when user is over a bar on the bar chart, but the log is output from the time the page loads.
The running code is here, https://shanegibney.github.io/d3Mouseover/
The full code is here, https://github.com/shanegibney/d3Mouseover
<!DOCTYPE html>
<meta charset="utf-8">
<style type="text/css">
body {
font-family: avenir next, sans-serif;
font-size: 12px;
}
.zoom {
cursor: move;
fill: none;
pointer-events: all;
}
.axis {
stroke-width: 0.5px;
stroke: #888;
font: 10px avenir next, sans-serif;
}
.axis>path {
stroke: #888;
}
</style>
<body>
<div id="totalDistance">
</div>
</body>
<script src="https://d3js.org/d3.v4.min.js"></script>
<script>
/* Adapted from: https://bl.ocks.org/mbostock/34f08d5e11952a80609169b7917d4172 */
var margin = {
top: 20,
right: 20,
bottom: 90,
left: 50
},
margin2 = {
top: 230,
right: 20,
bottom: 30,
left: 50
},
width = 960 - margin.left - margin.right,
height = 300 - margin.top - margin.bottom,
height2 = 300 - margin2.top - margin2.bottom;
var parseTime = d3.timeParse("%Y-%m-%d %H:%M");
var x = d3.scaleTime().range([0, width]),
x2 = d3.scaleTime().range([0, width]),
y = d3.scaleLinear().range([height, 0]),
y2 = d3.scaleLinear().range([height2, 0]),
dur = d3.scaleLinear().range([0, 12]);
var xAxis = d3.axisBottom(x).tickSize(0),
xAxis2 = d3.axisBottom(x2).tickSize(0),
yAxis = d3.axisLeft(y).tickSize(0);
var brush = d3.brushX()
.extent([
[0, 0],
[width, height2]
])
.on("start brush end", brushed);
var zoom = d3.zoom()
.scaleExtent([1, Infinity])
.translateExtent([
[0, 0],
[width, height]
])
.extent([
[0, 0],
[width, height]
])
.on("zoom", zoomed);
var svg = d3.select("body").append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom);
svg.append("defs").append("clipPath")
.attr("id", "clip")
.append("rect")
.attr("width", width)
.attr("height", height);
var focus = svg.append("g")
.attr("class", "focus")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
var context = svg.append("g")
.attr("class", "context")
.attr("transform", "translate(" + margin2.left + "," + margin2.top + ")");
d3.json("dataDefault.json", function(error, data) {
if (error) throw error;
var parseTime = d3.timeParse("%Y-%m-%d %H:%M");
var mouseoverTime = d3.timeFormat("%a %e %b %Y %H:%M");
var minTime = d3.timeFormat("%b%e, %Y");
var parseDate = d3.timeParse("%b %Y");
data.forEach(function(d) {
d.mouseoverDisplay = parseTime(d.date);
d.date = parseTime(d.date);
d.end = parseTime(d.end);
d.duration = ((d.end - d.date) / (60 * 1000)); // session duration in minutes
d.distance = +d.distance;
d.intensityInverted = (1 / (d.distance / d.duration)); // inverse of intensity so that the light colour is for low intensity and dark colour is for high intensity
d.intensity = Math.round(d.distance / d.duration); // actually intensity, metres per minute.
d.course = d.course.toLowerCase();
return d;
},
function(error, data) {
if (error) throw error;
});
var total = 0;
data.forEach(function(d) {
total = d.distance + total;
});
var minDate = d3.min(data, function(d) {
return d.date;
});
total = String(total).replace(/(.)(?=(\d{3})+$)/g, '$1,') //place thousands comma in total distance string
var xMin = d3.min(data, function(d) {
return d.date;
});
var yMax = Math.max(20, d3.max(data, function(d) {
return d.distance;
}));
dur.domain([0, d3.max(data, function(d) {
return d.duration;
})]);
var colorScale = d3.scaleSequential(d3.interpolateInferno)
.domain([0, d3.max(data, function(d) {
return d.intensityInverted;
})]);
var totalDistance = d3.select("#totalDistance").append("p").text("Total distance: " + total + "m");
x.domain([xMin, new Date()]);
y.domain([0, yMax]);
x2.domain(x.domain());
y2.domain(y.domain());
// append scatter plot to main chart area
var messages = focus.append("g");
messages.attr("clip-path", "url(#clip)");
messages.selectAll("message")
.data(data)
.enter().append("rect")
.style("fill", function(d) {
return colorScale(d.intensityInverted);
})
.attr("class", "message")
.attr("x", function(d) {
return x(d.date);
})
.attr("y", function(d) {
return y(d.distance);
})
.attr("width", function(d) {
return dur(d.duration);
})
.attr("height", function(d) {
return height - y(d.distance);
})
// .on("mouseover", onit);
.on("mouseover", () => console.log("I am now on it for sure!"));
// function onit(d) {
// console.log("I am on it now!")
// }
focus.append("g")
.attr("class", "axis x-axis")
.attr("transform", "translate(0," + height + ")")
.call(xAxis);
focus.append("g")
.attr("class", "axis axis--y")
.call(yAxis);
// Summary Stats
focus.append("text")
.attr("transform", "rotate(-90)")
.attr("y", 0 - margin.left)
.attr("x", 0 - (height / 2))
.attr("dy", "1em")
.style("text-anchor", "middle")
.text("Distance in meters");
// focus.append("text")
// .attr("x", width - margin.right)
// .attr("dy", "1em")
// .attr("text-anchor", "end")
// // .text("Messages: " + num_messages(data, x));
// .text("Total distance: " + total + "m");
svg.append("text")
.attr("transform",
"translate(" + ((width + margin.right + margin.left) / 2) + " ," +
(height + margin.top + margin.bottom) + ")")
.style("text-anchor", "middle")
.text("Date");
svg.append("rect")
.attr("class", "zoom")
.attr("width", width)
.attr("height", height)
.attr("transform", "translate(" + margin.left + "," + margin.top + ")")
.call(zoom);
// append scatter plot to brush chart area
var messages = context.append("g");
messages.attr("clip-path", "url(#clip)");
messages.selectAll("message")
.data(data)
.enter().append("rect")
.style("fill", function(d) {
return colorScale(d.intensityInverted);
})
.attr("class", "message")
.attr("x", function(d) {
return x2(d.date);
})
.attr("y", function(d) {
return y2(d.distance);
})
.attr("width", function(d) {
return dur(d.duration);
})
.attr("height", function(d) {
return height2 - y2(d.distance);
});
context.append("g")
.attr("class", "axis x-axis")
.attr("transform", "translate(0," + height2 + ")")
.call(xAxis2);
context.append("g")
.attr("class", "brush")
.call(brush)
.call(brush.move, x.range());
});
//create brush function redraw scatterplot with selection
function brushed() {
if (d3.event.sourceEvent && d3.event.sourceEvent.type === "zoom") return; // ignore brush-by-zoom
var s = d3.event.selection || x2.range();
x.domain(s.map(x2.invert, x2));
focus.selectAll(".message")
.attr("x", function(d) {
return x(d.date);
})
.attr("y", function(d) {
return y(d.distance);
})
.attr("width", function(d) {
return dur(d.duration);
})
.attr("height", function(d) {
return height - y(d.distance);
});
focus.select(".x-axis").call(xAxis);
svg.select(".zoom").call(zoom.transform, d3.zoomIdentity
.scale(width / (s[1] - s[0]))
.translate(-s[0], 0));
var e = d3.event.selection;
var selectedMessages = focus.selectAll('.message').filter(function() {
var xValue = this.getAttribute('x');
return e[0] <= xValue && xValue <= e[1];
});
// console.log(selectedMessages.nodes().length);
}
function zoomed() {
if (d3.event.sourceEvent && d3.event.sourceEvent.type === "brush") return; // ignore zoom-by-brush
var t = d3.event.transform;
x.domain(t.rescaleX(x2).domain());
focus.selectAll(".message")
// .enter().append("rect")
// .style("fill", function(d) {
// return colorScale(d.intensityInverted);
// })
// .attr("class", "message")
.attr("x", function(d) {
return x(d.date);
})
.attr("y", function(d) {
return y(d.distance);
})
.attr("width", function(d) {
return dur(d.duration);
})
.attr("height", function(d) {
return height - y(d.distance);
});
focus.select(".x-axis").call(xAxis);
context.select(".brush").call(brush.move, x.range().map(t.invertX, t));
}
function handleMouseOver(d) {
d3.select(this)
.style("fill", "lightBlue");
g.select('text')
.attr("x", 15)
.attr("y", 5)
.text("Session no. " + d.number)
.append('tspan')
.text("Date: " + mouseoverTime(d.mouseoverDisplay))
.attr("x", 15)
.attr("y", 30)
.append('tspan')
.text("Distance: " + d.distance + "m")
.attr("x", 15)
.attr("y", 50)
.append('tspan')
.text("Duration: " + d.duration + " mins")
.attr("x", 15)
.attr("y", 70)
.append('tspan')
.text("Intensity: " + d.intensity + " meters/mins")
.attr("x", 15)
.attr("y", 90)
.append('tspan')
.text("Pool: " + d.pool + " (" + d.course + ")")
.attr("x", 15)
.attr("y", 110);
console.log("handleMouseOver function");
}
function handleMouseOut(d) {
d3.select(this)
.style("fill", function(d) {
return colorScale(d.intensityInverted);
});
g.select('text').text("Total distance since " + minTime(minDate) + ": " + total + "m");
}
</script>
Sample data,
[{
"number": "1",
"date": "2016-11-09 11:15",
"end": "2016-11-09 11:45",
"distance": "1100",
"course": "LC",
"pool": "UCD"
},
{
"number": "2",
"date": "2016-11-10 10:40",
"end": "2016-11-10 11:20",
"distance": "1500",
"course": "LC",
"pool": "UCD"
},
{
"number": "3",
"date": "2016-11-11 16:45",
"end": "2016-11-11 17:50",
"distance": "2000",
"course": "LC",
"pool": "UCD"
},
{
"number": "4",
"date": "2016-11-12 12:48",
"end": "2016-11-12 13:53",
"distance": "2500",
"course": "LC",
"pool": "UCD"
}
]
I added the last two lines here, but still no luck,
var messages = focus.append("g");
messages.attr("clip-path", "url(#clip)");
messages.selectAll("message")
.data(data)
.enter().append("rect")
.style("fill", function(d) {
return colorScale(d.intensityInverted);
})
.attr("class", "message")
.attr("x", function(d) {
return x(d.date);
})
.attr("y", function(d) {
return y(d.distance);
})
.attr("width", function(d) {
return dur(d.duration);
})
.attr("height", function(d) {
return height - y(d.distance);
})
// .on("mouseover", onit);
.on("mouseover", () => console.log("I am now on it for sure!"));
// function onit(d) {
// console.log("I am on it now!")
// }
I suspect the problem is because I am using brushX() and zoom.
When you do this:
.on("mouseover", console.log("I am on it"))
You're passing the result of the console.log function to the callback. Instead of that, you want to pass its reference:
.on("mouseover", function(){
console.log("I am on it")
})
Check this snippet (don't hover over the circle!):
var circle = d3.select("circle");
circle.on("mouseover", console.log("I'm on it!"));
<script src="https://d3js.org/d3.v4.min.js"></script>
<svg>
<circle cx="50" cy="50" r="15" fill="teal"></circle>
</svg>
Now the same code, with the reference to console.log:
var circle = d3.select("circle");
circle.on("mouseover", () => console.log("I'm on it!"));
<script src="https://d3js.org/d3.v4.min.js"></script>
<svg>
<circle cx="50" cy="50" r="15" fill="teal"></circle>
</svg>
The issue is that you are calling the function at the time you are declaring it:
.on("mouseover", console.log("I am on it")) //function call
should be something like this:
.on("mouseover", console.log) //function ref
.on("mouseover", function(d) { console.log("I am on it") }) //function ref
hi I created a spiral chart in d3.js, and I want to add circle to different position of the spiral lines.according to there values.
circle closes to the center will have highest priority.
any idea how to do that.
here is the code which i wrote
var width = 400,
height = 430
num_axes = 8,
tick_axis = 1,
start = 0
end = 4;
var theta = function(r) {
return -2*Math.PI*r;
};
var arc = d3.svg.arc()
.startAngle(0)
.endAngle(2*Math.PI);
var radius = d3.scale.linear()
.domain([start, end])
.range([0, d3.min([width,height])/2-20]);
var angle = d3.scale.linear()
.domain([0,num_axes])
.range([0,360])
var svg = d3.select("#chart").append("svg")
.attr("width", width)
.attr("height", height)
.append("g")
.attr("transform", "translate(" + width/2 + "," + (height/2+8) +")");
var pieces = d3.range(start, end+0.001, (end-start)/1000);
var spiral = d3.svg.line.radial()
.interpolate("cardinal")
.angle(theta)
.radius(radius);
//svg.append("text")
// .text("And there was much rejoicing!")
// .attr("class", "title")
// .attr("x", 0)
// .attr("y", -height/2+16)
// .attr("text-anchor", "middle")
//svg.selectAll("circle.tick")
// .data(d3.range(end,start,(start-end)/4))
// .enter().append("circle")
// .attr("class", "tick")
// .attr("cx", 0)
// .attr("cy", 0)
// .attr("r", function(d) { return radius(d); })
svg.selectAll(".axis")
.data(d3.range(num_axes))
.enter().append("g")
.attr("class", "axis")
.attr("transform", function(d) { return "rotate(" + -angle(d) + ")"; })
.call(radial_tick)
.append("text")
.attr("y", radius(end)+13)
.text(function(d) { return angle(d) + "°"; })
.attr("text-anchor", "middle")
.attr("transform", function(d) { return "rotate(" + -90 + ")" })
svg.selectAll(".spiral")
.data([pieces])
.enter().append("path")
.attr("class", "spiral")
.attr("d", spiral)
.attr("transform", function(d) { return "rotate(" + 90 + ")" });
function radial_tick(selection) {
selection.each(function(axis_num) {
d3.svg.axis()
.scale(radius)
.ticks(5)
.tickValues( axis_num == tick_axis ? null : [])
.orient("bottom")(d3.select(this))
d3.select(this)
.selectAll("text")
.attr("text-anchor", "bottom")
.attr("transform", "rotate(" + angle(axis_num) + ")")
});
}
please see the second solution for my implementation. Help me with connecting the circle with the center
Here is a model for the technique you seem to be looking for...
var width = 400,
height = 430,
num_axes = 8,
tick_axis = 1,
start = 0,
end = 4,
testValue = 2;
var theta = function (r) {
return -2 * Math.PI * r;
};
var arc = d3.svg.arc()
.startAngle(0)
.endAngle(2 * Math.PI);
var radius = d3.scale.linear()
.domain([start, end])
.range([0, (d3.min([width, height]) / 2 - 20)]);
var angle = d3.scale.linear()
.domain([0, num_axes])
.range([0, 360]);
var chart = d3.select("#chart")
.style("width", width + "px");
var svg = d3.select("#chart").append("svg")
.attr("width", width)
.attr("height", height)
.append("g")
.attr("transform", "translate(" + width / 2 + "," + (height / 2 + 8) + ")");
var pieces = d3.range(start, end + 0.001, (end - start) / 500);
var spiral = d3.svg.line.radial()
.interpolate("linear")
.angle(theta)
.radius(radius);
svg.append("text")
.text("Title")
.attr("class", "title")
.attr("x", 0)
.attr("y", -height/2+16)
.attr("text-anchor", "middle")
svg.selectAll("circle.tick")
.data(d3.range(end,start,(start-end)/4))
.enter().append("circle")
.attr("class", "tick")
.style({fill: "black", opacity: 0.1})
.attr("cx", 0)
.attr("cy", 0)
.attr("r", function(d) { return radius(d); })
svg.selectAll(".axis")
.data(d3.range(num_axes))
.enter().append("g")
.attr("class", "axis")
.attr("transform", function (d) { return "rotate(" + -angle(d) + ")"; })
.call(radial_tick)
.append("text")
.attr("y", radius(end) + 13)
.text(function (d) { return angle(d) + "°"; })
.attr("text-anchor", "middle")
.attr("transform", function (d) { return "rotate(" + -90 + ")" })
svg.selectAll(".axis path")
.style({fill: "none", stroke: "black"})
.attr("stroke-dasharray", "5 5")
svg.selectAll(".spiral")
.data([pieces])
.enter().append("path")
.attr("class", "spiral")
.attr("fill", "none")
.attr("stroke", "black")
.attr("d", spiral)
.attr("transform", function (d) { return "rotate(" + 90 + ")" });
function radial_tick(selection) {
selection.each(function (axis_num) {
d3.svg.axis()
.scale(radius)
.ticks(5)
.tickValues(axis_num == tick_axis ? null : [])
.tickSize(1)
.orient("bottom")(d3.select(this))
d3.select(this)
.selectAll("text")
.attr("text-anchor", "bottom")
.attr("transform", "rotate(" + angle(axis_num) + ")")
});
}
function radialScale(x) {
var t = theta(x), r = radius(x);
d3.select(this)
.attr("cx", r * Math.cos(t))
.attr("cy", r * Math.sin(t))
}
slider = SliderControl("#circleSlider", "data", update, [start, end], ",.3f");
function update(x) {
if (typeof x != "undefined") testValue = x;
var circles = svg.selectAll(".dataPoints")
.data([testValue]);
circles.enter().append("circle");
circles.attr("class", "dataPoints")
.style({ fill: "black", opacity: 0.6 })
.attr("r", 10)
.each(radialScale)
circles.exit().remove();
return testValue
}
function SliderControl(selector, title, value, domain, format) {
var accessor = d3.functor(value), rangeMax = 1000,
_scale = d3.scale.linear().domain(domain).range([0, rangeMax]),
_$outputDiv = $("<div />", { class: "slider-value" }),
_update = function (value) {
_$outputDiv.css("left", 'calc( '
+ (_$slider.position().left + _$slider.outerWidth()) + 'px + 1em )')
_$outputDiv.text(d3.format(format)(value));
$(".input").width(_$outputDiv.position().left + _$outputDiv.outerWidth() - _innerLeft)
},
_$slider = $(selector).slider({
value: _scale(accessor()),
max: rangeMax,
slide: function (e, ui) {
_update(_scale.invert(ui.value));
accessor(_scale.invert(ui.value));
}
}),
_$wrapper = _$slider.wrap("<div class='input'></div>")
.before($("<div />").text(title + ":"))
.after(_$outputDiv).parent(),
_innerLeft = _$wrapper.children().first().position().left;
_update(_scale.invert($(selector).slider("value")))
return d3.select(selector)
};
.domain {
stroke-width: 1px;
}
<link href="https://ajax.googleapis.com/ajax/libs/jqueryui/1.11.4/themes/smoothness/jquery-ui.css" rel="stylesheet"/>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/jqueryui/1.11.4/jquery-ui.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
<div id="chart">
<div id="circleSlider"></div>
</div>