I'm creating an org chart in D3 based on Bernhard Zuba's D3.js Organizational Chart. The org chart models an organization in which any given person (represented by a white square) may have a hundred or so people immediately beneath them (a very flat tree structure with a black bezier curve representing each parent-child relationship).
Here's a screencap of part of the tree:
And here's a zoom-in on the bottom of the parent node in the above picture:
The problem is that the links between child and parent nodes tend to all bunch up together, resulting in a very thick black line with a very gradual slope, which can be a bit of an eyesore.
The function I'm using to generate the links is as follows:
// Diagonal function
var diagonal = d3.svg.diagonal()
.source(function (d) {
return {
x: d.source.x + (rectW / 2),
y: d.source.y + rectH - 10
};
})
.target(function (d) {
return {
x: d.target.x + (rectW / 2),
y: d.target.y + 10
};
})
.projection(function (d) {
return [d.x, d.y];
});
Here, rectW is the width of each node and rectH is the height of each node.
What I'd like to do is make some slight adjustments to the bezier function used to generate the links. Specifically, I'd like to flatten out the control points a little so that the curves at the start and end of the curve are more dramatic. If anyone can show me how to alter the function used by diagonal() to generate the bezier curve, I can figure out the rest.
If you look at the source code to svg.diagonal, I can't really see a direct way to adjust just the control points. You'd think you could use projection for this, but that'll transform all 4 points used to generate the path. Now, I guess we could get a little hacky with projection and do something like this:
<!DOCTYPE html>
<html>
<head>
<script data-require="d3#3.5.17" data-semver="3.5.17" src="https://d3js.org/d3.v3.min.js"></script>
</head>
<body>
<script>
var data = [{
source: {
x: 10,
y: 10
},
target: {
x: 200,
y: 200
}
}, {
source: {
x: 50,
y: 50
},
target: {
x: 200,
y: 200
}
}];
var svg = d3.select('body')
.append('svg')
.attr('width', 205)
.attr('height', 205);
var diagonal = d3.svg.diagonal()
.projection(function(d) {
if (!this.times) this.times = 0;
this.times++;
console.log(this.times);
if (this.times === 1) {
return [d.x, d.y];
} else if (this.times === 2) {
return [d.x - 25, d.y]
} else if (this.times === 3) {
return [d.x + 25, d.y];
} else if (this.times === 4) {
this.times = 0;
return [d.x, d.y];
}
});
svg.selectAll('path')
.data(data)
.enter()
.append('path')
.attr('d', diagonal)
.style('fill', 'none')
.style('stroke', 'black');
</script>
</body>
</html>
I might be over thinking this. You'd probably just be better drawing the arc yourself:
<!DOCTYPE html>
<html>
<head>
<script data-require="d3#3.5.17" data-semver="3.5.17" src="https://d3js.org/d3.v3.min.js"></script>
</head>
<body>
<script>
var data = [{
source: {
x: 10,
y: 10
},
target: {
x: 200,
y: 200
}
}, {
source: {
x: 200,
y: 10
},
target: {
x: 10,
y: 200
}
}];
var svg = d3.select('body')
.append('svg')
.attr('width', 205)
.attr('height', 205);
svg.selectAll('path')
.data(data)
.enter()
.append('path')
.attr('d', function(d){
var s = d.source,
t = d.target,
m = (s.y + t.y) / 2,
p0 = [s.x, s.y],
p1 = [s.x, m],
p2 = [t.x, m],
p3 = [t.x, t.y];
// adjust constrol points
p1[0] -= 25;
p2[0] += 25;
return "M" + p0 + "C" + p1 + " " + p2 + " " + p3;
})
.style('fill', 'none')
.style('stroke', 'black');
</script>
</body>
</html>
Related
I'm fairly new to D3 and I am trying to use d3.js & Lasso in order to allow users to select dots on a scatterplot. I've found an example of how to do this right here: http://bl.ocks.org/skokenes/511c5b658c405ad68941
This works perfectly fine in D3 with V5 but I have a requirement to upgrade to D3 V6 and the code breaks.
d3-lasso.js:819 Uncaught TypeError: Cannot read property 'sourceEvent' of undefined
at SVGRectElement.dragmove (d3-lasso.js:819)
at nt.call (d3.min.js:2)
at Object.e [as mouse] (d3.min.js:2)
at p (d3.min.js:2)
at d3.min.js:2
Any help would be appreciated ?
-- 2023 solution for D3.v7
I rewrite the lasso function from scratch using the d3 drag event.
Below shows you an example of how to perform lasso selection on a scatter plot. The basic idea is to render a path when your cursor moves on the screen and determine which points are inside your polygon using an existing algorithm.
<!DOCTYPE html>
<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.0">
<title>d3-lasso</title>
<script src="https://d3js.org/d3.v7.min.js"></script>
<style>
#chart {
width: 600px;
height: 400px;
background-color: wheat;
}
</style>
</head>
<body>
<div>
<svg id="chart">
</svg>
</div>
<script>
const data = [
{ x: -4, y: 14, id: 1 },
{ x: -26, y: -49, id: 2 },
{ x: 28, y: 11, id: 3 },
{ x: 0, y: -30, id: 4 },
{ x: 6.9, y: -63, id: 5 }
];
let xScale = d3
.scaleLinear()
.domain(d3.extent(data.map((d) => d.x)))
.range([50, 550]);
let yScale = d3
.scaleLinear()
.domain(d3.extent(data.map((d) => d.y)))
.range([50, 350]);
const circles = d3
.select("#chart")
.selectAll("circle")
.data(data)
.join("circle")
.attr("id", (d) => {
return "dot-" + d.id;
})
.attr("cx", (d) => {
return xScale(d.x);
})
.attr("cy", (d) => {
return yScale(d.y);
})
.attr("r", 10)
.attr("opacity", 0.5)
.attr("fill", 'steelblue');
// lasso selection based on the drag events
let coords = [];
const lineGenerator = d3.line();
const pointInPolygon = function (point, vs) {
// console.log(point, vs);
// ray-casting algorithm based on
// https://wrf.ecse.rpi.edu/Research/Short_Notes/pnpoly.html/pnpoly.html
var x = point[0],
y = point[1];
var inside = false;
for (var i = 0, j = vs.length - 1; i < vs.length; j = i++) {
var xi = vs[i][0],
yi = vs[i][1];
var xj = vs[j][0],
yj = vs[j][1];
var intersect =
yi > y != yj > y &&
x < ((xj - xi) * (y - yi)) / (yj - yi) + xi;
if (intersect) inside = !inside;
}
return inside;
};
function drawPath() {
d3.select("#lasso")
.style("stroke", "black")
.style("stroke-width", 2)
.style("fill", "#00000054")
.attr("d", lineGenerator(coords));
}
function dragStart() {
coords = [];
circles.attr("fill", "steelblue");
d3.select("#lasso").remove();
d3.select("#chart")
.append("path")
.attr("id", "lasso");
}
function dragMove(event) {
let mouseX = event.sourceEvent.offsetX;
let mouseY = event.sourceEvent.offsetY;
coords.push([mouseX, mouseY]);
drawPath();
}
function dragEnd() {
let selectedDots = [];
circles.each((d, i) => {
let point = [
xScale(d.x),
yScale(d.y),
];
if (pointInPolygon(point, coords)) {
d3.select("#dot-" + d.id).attr("fill", "red");
selectedDots.push(d.id);
}
});
console.log(`select: ${selectedDots}`);
}
const drag = d3
.drag()
.on("start", dragStart)
.on("drag", dragMove)
.on("end", dragEnd);
d3.select("#chart").call(drag);
</script>
</body>
</html>
I have a project in which I am using nvd3.js and want to create a semi-circular gauge. How do I create it?
I did try to use guage.js for the same but the problem I am facing there is that I cannot add customized/tags labels for the gauge that are of string type and I could not find a way around the problem. Any help is appreciated.
This is what I came up with. It's not perfect, the tooltips are rather meaningless. Also I've been trying to come up with a gradient fill to give it a 3D look, but haven't managed to yet.
<div style="height: 200px; width: 350px;">
<div style="height: 350px; width: 350px;">
<svg id="gaugeContainer"></svg>
</div>
</div>
<script>
//d3.json("/api/gauge", updateChart);
updateChart({ "max": 100, "current": 90 });
function updateChart(data) {
var containerSvg = "gaugeContainer";
var height = 350;
var width = 350;
// The first graph section always starts at 0 degrees, but we
// want it to be horizontal so we rotate the whole thing -90
// degrees, then correct the labels.
// The formula for needle angle is ((current/max) * 180) - 90
// current / max will always be 1.0 or less,
// the fraction of max that the needle indicates,
// * 180 converts to degrees and -90 compensates for rotation.
var indicatorAngle = ((data.current / data.max) * 180) - 90;
var colors = ["green", "orange", "red", "white"];
// Apparently after 4 sections are defined, the fifth one is
// dead, as in not graphed. We play a little trick, defining a
// very small white section so we can display
// the max value at the end of its visible range.
// The key values could just as easily be strings.
var graphData = [
{ key: Math.round((data.max / 8) * 1), y: 1 },
{ key: Math.round((data.max / 8) * 3), y: 1 },
{ key: Math.round((data.max / 8) * 6), y: 2 },
{ key: Math.round((data.max / 8) * 8), y: 0.2 },
{ key: "", y: 3.8 }
];
var arcRadius = [
{ inner: 0.6, outer: 1 },
{ inner: 0.6, outer: 1 },
{ inner: 0.6, outer: 1 },
{ inner: 0.6, outer: 1 }
];
nv.addGraph(function () {
var chart = nv.models.pieChart()
.x(function (d) { return d.key })
.y(function (d) { return d.y })
.donut(true)
.showTooltipPercent(false)
.width(width)
.height(height)
.color(colors)
.arcsRadius(arcRadius)
.donutLabelsOutside(true)
.showLegend(false)
.growOnHover(false)
.labelSunbeamLayout(false);
d3.select("#" + containerSvg)
.datum(graphData)
.transition().duration(1)
.attr('width', width)
.attr('height', height)
.attr("transform", "rotate(-90)")
.call(chart);
// draw needle
d3.select("#" + containerSvg)
.append("path")
.attr("class", "line")
.attr("fill", "none")
.style("stroke", "gray")
.style("stroke-width", "1")
.attr("d", "M0, -3 L153, 0 0,3 Z")
.attr("transform", "translate(175,179) rotate(" + indicatorAngle + ")");
d3.select("#" + containerSvg)
.append("circle")
.attr("r", "6")
.attr("cx", "0")
.attr("cy", "0")
.style("stroke", "gray")
.style("stroke-width", "1")
.attr("transform", "translate(175,179) rotate(" + indicatorAngle + ")");
// correct text rotation
d3.select("#" + containerSvg).selectAll("text")
.attr("transform", "rotate(90)");
return chart;
});
}
</script>
I'm trying to create a visual that is a modification of the Arc Tween demo. In it, I want an array of data to define the colors and general radius of each arc, and on an interval, both the start and end angles should slowly animate. But I think the way I'm defining each arc is causing things to over animate.
HTML
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width">
<script src="https://d3js.org/d3.v4.min.js"></script>
<title>Arcs</title>
</head>
<body>
<svg width="960" height="500"></svg>
</body>
</html>
SCRIPT
// Modified from Arc Tween
// https://bl.ocks.org/mbostock/5100636
var tau = 2 * Math.PI; // http://tauday.com/tau-manifesto
var overlap = 50
var jsonArcs = [
{ "base_radius": 370, "color" : "red"},
{ "base_radius": 330, "color" : "orange"},
{ "base_radius": 290, "color" : "yellow"},
{ "base_radius": 250, "color" : "green"},
{ "base_radius": 210, "color" : "blue" },
{ "base_radius": 170, "color" : "purple"},
{ "base_radius": 130, "color" : "black"},
{ "base_radius": 90, "color" : "red"}
];
var arc = d3.arc()
.startAngle(function(d) { return Math.random() * tau; })
.endAngle(function(d) { return Math.random() * tau; })
.innerRadius(function(d) { return d.base_radius - overlap * Math.random(); })
.outerRadius(function(d) { return d.base_radius + overlap * Math.random(); });
var center_def = d3.arc()
.innerRadius(0)
.outerRadius(60)
.startAngle(0);
var svg = d3.select("svg"),
width = +svg.attr("width"),
height = +svg.attr("height"),
g = svg.append("g").attr("transform", "translate(" + width / 2 + "," + height / 2 + ")");
var path = g.selectAll("path")
.data(jsonArcs)
.enter().append("path")
.attr("fill", function(d, i) { return d.color; })
.attr("d", arc);
var center = g.append("path")
.datum({endAngle: tau})
.style("fill", "black")
.attr("d", center_def);
d3.interval(function() {
path.transition()
.duration(750)
.attrTween("d", arcTween(Math.random() * tau, arc));
}, 2500);
function arcTween(newAngle, obj) {
return function(d) {
var interpolate = d3.interpolate(d.endAngle, newAngle);
return function(t) {
d.endAngle = interpolate(t);
return obj(d);
};
};
}
Instead of each arc smoothly animating from start angles to new angles, the entire visual jumps multiple times to a new state.
How do I configure this function such that each arc smoothly transitions its start and end angles from old to new?
There are a few issues here
Your arc function is going to return random radii every call, which is not what I think you want. You could transition an arc from one inner/outer radius to the next, but for simplicity, let's say that each path only initially gets a random radius
In order to transition from one the old pair of start/end angles to the new one you'll need to store the current angles somewhere. We'll store it in a local variable, which will be tied to each path
Because each path will have a different inner/outer radius, we'll need to have a different arc function for each segment as well.
working code here:
var tau = 2 * Math.PI; // http://tauday.com/tau-manifesto
var overlap = 50;
var currentSegment = d3.local();
var segmentRadius = d3.local();
var jsonArcs = [
{ "base_radius": 370, "color" : "red"},
{ "base_radius": 330, "color" : "orange"},
{ "base_radius": 290, "color" : "yellow"},
{ "base_radius": 250, "color" : "green"},
{ "base_radius": 210, "color" : "blue" },
{ "base_radius": 170, "color" : "purple"},
{ "base_radius": 130, "color" : "black"},
{ "base_radius": 90, "color" : "red"}
];
var arc = d3.arc()
.innerRadius(function() { return segmentRadius.get(this).innerRadius })
.outerRadius(function() { return segmentRadius.get(this).outerRadius });
var center_def = d3.arc()
.innerRadius(0)
.outerRadius(60)
.startAngle(0);
var svg = d3.select("svg"),
width = +svg.attr("width"),
height = +svg.attr("height"),
g = svg.append("g").attr("transform", "translate(" + width / 2 + "," + height / 2 + ")");
var path = g.selectAll("path")
.data(jsonArcs)
.enter().append("path")
.attr("fill", function(d, i) { return d.color; })
.each(function(d) {
var angles = randomAngles();
d.startAngle = angles.startAngle;
d.endAngle = angles.endAngle;
segmentRadius.set(this, {
innerRadius: d.base_radius - overlap * Math.random(),
outerRadius: d.base_radius + overlap * Math.random()
});
})
.attr("d", arc)
.each(function(d) {currentSegment.set(this, d)});
var center = g.append("path")
.datum({endAngle: tau})
.style("fill", "black")
.attr("d", center_def);
d3.interval(function() {
path.transition()
.duration(750)
.attrTween("d", arcTween);
}, 2500);
function arcTween() {
var thisPath = this;
var interpolate = d3.interpolate(currentSegment.get(this), randomAngles());
currentSegment.set(this, interpolate(0));
return function(t) {
return arc.call(thisPath, interpolate(t));
};
}
function randomAngles() {
var angles = [Math.random() * tau, Math.random() * tau].sort();
return {startAngle: angles[0], endAngle: angles[1]};
}
Notice a few things about the changed code:
I set the random initial angles in an each call on the path just before the "d" attribute is set
I stored the segment radii in the segmentRadius d3.local variable at the end of that same chain and also after each interpolation is set in the call to the transition
In the transition function I needed to call arc to preserve the 'this' of the path so that 'this' would be correct in arc.innerRadius when retrieving the segmentRadius.
d3.interpolate can happily handle objects and not just numbers.
I have a similar example that I've been working on here if you'd like to learn more.
Thisis an example by Mike Bostock of a "simple" hive graph (as he refers to it in this article ). It has three "axis" created by this code
svg.selectAll(".axis")
.data(d3.range(3))
.enter().append("line")
.attr("class", "axis")
.attr("transform", function(d) { return "rotate(" + degrees(angle(d)) + ")"; })
.attr("x1", radius.range()[0])
.attr("x2", radius.range()[1]);
As you can see from the first link, the three "axes" form a circle, which seems to be accomplished by the rotation in the "transform" of the code above and use of these angle and degrees functions
var angle = d3.scale.ordinal().domain(d3.range(4)).rangePoints([0, 2 * Math.PI]),
function degrees(radians) {
return radians / Math.PI * 180 - 90;
}
Question: if there were only two "axes", how would it be possible (using a "translate") to stack the "axes" on top of each other (i.e. as two horizontal lines parallel to each other?
In my attempt to do this, I tried to remove the rotation of the "axis" and then to space them vertically. To stop the rotation,I removed the call to "degrees" like this
.attr("transform", function(d) { return "rotate(" + angle(d) + ")"; })
and I also set the range of the angles to be 0,0
d3.scale.ordinal().domain(["one", "two"]).range([0,0]);
then , to space the axes, I included a "translate" like this
.attr("transform", function(d) {return "translate(" + width /2 + "," + height/d + ")"});
The result is that there is one visible horizontal axis, and it seems the other one exists but is only detectable when I run the mouse over it ( and the nodes and lines haven't been repositioned)
Not sure if this is what you are after but two "axis" stacked vertically can be achieved with:
var angle = d3.scale.ordinal()
.domain(d3.range(3)) //<-- only calculate angles for 2 [-90, 90]
.rangePoints([0, 2 * Math.PI]),
...
svg.selectAll(".axis")
.data(d3.range(2)) //<-- 2 lines
EDITS
What are you are describing is not really a hive plot and attempting to re-purpose the layout is probably more trouble then it's worth. If you just want linked points on a line, here's an off-the-cuff implementation:
<!DOCTYPE html>
<meta charset="utf-8">
<style>
.link {
fill: none;
stroke-width: 1.5px;
}
.axis, .node {
stroke: #000;
stroke-width: 1.5px;
}
</style>
<body>
<script src="//d3js.org/d3.v3.min.js"></script>
<script src="d3.hive.min.js"></script>
<script>
var width = 500,
height = 500;
var lineSep = 200,
lineLen = 400,
color = d3.scale.category10().domain(d3.range(20)),
margin = [50,50];
var nodes = [
{x: 0, y: .1},
{x: 0, y: .9},
{x: 0, y: .2},
{x: 1, y: .3},
{x: 1, y: .1},
{x: 1, y: .8},
{x: 1, y: .4},
{x: 1, y: .6},
{x: 1, y: .2},
{x: 1, y: .7},
{x: 1, y: .8}
];
var links = [
{source: nodes[0], target: nodes[3]},
{source: nodes[1], target: nodes[3]},
{source: nodes[2], target: nodes[4]},
{source: nodes[2], target: nodes[9]},
{source: nodes[3], target: nodes[0]},
{source: nodes[4], target: nodes[0]},
{source: nodes[5], target: nodes[1]}
];
var nodeNest = d3.nest().key(function(d){ return d.x }).entries(nodes);
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height)
.append("g")
.attr("transform", "translate(" + margin[0] + "," + margin[1] + ")");
var axis = svg.selectAll(".axis")
.data(nodeNest);
var g = axis
.enter().append("g")
.attr("class", "axis")
.attr("transform", function(d,i) {
var t = "translate(0," + (i * lineSep) + ")";
return t;
})
.append("line")
.attr("x1", 0)
.attr("x2", lineLen);
axis.selectAll(".node")
.data(function(d){
d.values.forEach(function(q){
q.len = d.values.length;
})
return d.values;
})
.enter().append("circle")
.attr("class", "node")
.attr("cx", function(d, i, j) {
d.cx = ((i + 0.5) * (lineLen / d.len));
d.cy = (j * lineSep);
return d.cx;
})
.attr("r", 5)
.style("fill", function(d) { return color(d.x); });
svg.selectAll(".link")
.data(links)
.enter().append("path")
.attr("class", "link")
.attr("d", function(d){
console.log(d);
var p = "";
p += "M" + d.source.cx + "," + d.source.cy;
p += "Q" + "0," + ((margin[1] / 2) + (lineSep/2));
p += " " + d.target.cx + "," + d.target.cy;
return p;
})
.style("stroke", function(d) {
return color(d.source.x);
});
function degrees(radians) {
return radians / Math.PI * 180 - 90;
}
</script>
I'm working with a D3 example file for a force directed voronoi graph... however, I mainly just needed a simplified version with only three vertices... so I simplified the file and have included a JSFiddle example of where my file stands currently. My issue is how the edge condition is handled. Right now, the voronoi edges extend out to the edge of the container div. However, I'd like to clip each voronoi cell by a circle boundary. I've included two images to help explain my problem. The first image shows the script as it exists now, whereas the second is made with photoshop - showing the circular clipping boundary. It seems like D3's polygon.clip method would be the best option, but I don't really know how to implement the clipping method into my script. Any suggestions would be greatly appreciated.
<!DOCTYPE html>
<html>
<head>
<title>Voronoi Diagram with Force Directed Nodes and Delaunay Links</title>
<meta http-equiv="Content-Type" content="text/html;charset=utf-8"/>
<script src="http://d3js.org/d3.v3.min.js"></script>
<style type="text/css">
path
{
stroke: #EFEDF5;
stroke-width: 4px;
}
</style>
</head>
<body>
<div id="chart">
</div>
<script type="text/javascript">
var w = window.innerWidth > 960 ? 960 : (window.innerWidth || 960),
h = window.innerHeight > 500 ? 500 : (window.innerHeight || 500),
radius = 5.25,
links = [],
simulate = true,
zoomToAdd = true,
cc = ["#FFA94A","#F58A3A","#F85A19"]
var numVertices = (w*h) / 200000;
var vertices = d3.range(numVertices).map(function(i) {
angle = radius * (i+10);
return {x: angle*Math.cos(angle)+(w/2), y: angle*Math.sin(angle)+(h/2)};
});
var d3_geom_voronoi = d3.geom.voronoi().x(function(d) { return d.x;}).y(function(d) { return d.y; })
var prevEventScale = 1;
var zoom = d3.behavior.zoom().on("zoom", function(d,i) {
if (zoomToAdd){
if (d3.event.scale > prevEventScale) {
angle = radius * vertices.length;
}
force.nodes(vertices).start()
} else {
if (d3.event.scale > prevEventScale) {
radius+= .01
} else {
radius -= .01
}
vertices.forEach(function(d, i) {
angle = radius * (i+10);
vertices[i] = {x: angle*Math.cos(angle)+(w/2), y: angle*Math.sin(angle)+(h/2)};
});
force.nodes(vertices).start()
}
prevEventScale = d3.event.scale;
});
d3.select(window)
.on("keydown", function() {
// shift
if(d3.event.keyCode == 16) {
zoomToAdd = false
}
})
.on("keyup", function() {
zoomToAdd = true
})
var svg = d3.select("#chart")
.append("svg")
.attr("width", w)
.attr("height", h)
.call(zoom)
var force = d3.layout.force()
.charge(-300)
.size([w, h])
.on("tick", update);
force.nodes(vertices).start();
var path = svg.selectAll("path");
function update(e) {
path = path.data(d3_geom_voronoi(vertices))
path.enter().append("path")
// drag node by dragging cell
.call(d3.behavior.drag()
.on("drag", function(d, i) {
vertices[i] = {x: vertices[i].x + d3.event.dx, y: vertices[i].y + d3.event.dy}
})
)
.style("fill", function(d, i) { return cc[0] })
path.attr("d", function(d) { return "M" + d.join("L") + "Z"; })
.transition().duration(150)
.style("fill", function(d, i) { return cc[i] })
path.exit().remove();
if(!simulate) force.stop()
}