d3.js Fill area between to diagonals - d3.js

I would to imitate the pink filled elemets in the image. I think those are two diagonals. But I dont know how to fill the area between them.
Any help, I would appreciate it too much!
This is the code that I have:
<!DOCTYPE html>
<meta charset="utf-8">
<body>
<script src="d3.min.js"></script>
<style>
.link {
fill: none;
stroke: #ccc;
stroke-width: 1.5px;
}
</style>
<div class="container">
<h1 class="page-header">Chart</h1>
<div class="chart"></div>
</div>
<script>
var links = [{source: {x:0,y:0}, target: {x:200,y:200},x:0,y:0},
{source: {x:0,y:0}, target: {x:170,y:200},x:0,y:0}
]
var canvas = d3.select(".chart").append("svg")
.attr("width", 200)
.attr("height", 200)
.append("g");
var linksContainer = canvas.append("g").attr("class","linksContainer")
var diagonal = d3.svg.diagonal()
.source(function(d) { return {"x":d.source.x, "y":d.source.y}; })
.target(function(d) { return {"x":d.target.x, "y":d.target.y};})
.projection(function(d) { return [d.x, d.y]; });
var link = linksContainer.selectAll(".link")
.data(links)
.enter()
.append("path")
.attr("class", "link")
.attr("d", diagonal);
var line = linksContainer.append("line")
.attr("x1", 170)
.attr("y1", 200)
.attr("x2", 200)
.attr("y2", 200)
.attr("stroke-width", 1)
.attr("stroke", "black");
</script>
</body>

d3.svg.diagonal() produces a String representing an SVG path description. In your case, the two paths Strings are
var path1 = diagonal(links[0]);// "M0,0C0,100 200,100 200,200"
and
var path2 = diagonal(links[1]);// "M0,0C0,100 170,100 170,200"
Your code currently assigns each of them to the d attribute of a <path class=".link" d="...">.
Instead, you need to make just a single <path> with a filled (hence closed) path string that is a combination of those two strings.
var shape = path1 + path2;// But not quite...
It turns out that you can't simply concat those two strings. One thing you need to do is replace the M — which picks up and moves the pen to a new point (thus interrupting the closed-ness of the shape) — with an L, which draws a line to that new point:
var path2 = diagonal(links[1]).replace(/^M/, 'L');// "L0,0C0,100 170,100 170,200"
That gives you this:
var shape = path1 + path2;// "M0,0C0,100 170,100 170,200 L0,0C0,100 170,100 170,200"
However, the sub-path ...170,200 L0,0... incorrectly connects the end of path1 to the start of path2. To correct this, you need to reverse path2 by swapping between the link's start and end points:
var links = [ {source: {x:0,y:0}, target: {x:200,y:200},x:0,y:0},
// instead of {source: {x:0,y:0}, target: {x:170,y:200},x:0,y:0}]
{source: {x:170,y:200}, target: {x:0,y:0},x:0,y:0}]
Finally, this might not make a difference visually, but for correctness, you should close the path, using the Z directive:
var shape = path1 + path2 + 'Z';
// Apply `shape` to a single path element:
var path = linksContainer.append('path')
.attr("class", "link")
.attr("d", shape);
Here's a working fiddle.

The picture shown is actually a chord diagram layout.
You can learn how to use it here
https://github.com/mbostock/d3/wiki/Chord-Layout
Diagonals are different shapes that are used for tree diagrams

Related

Curved geojson polygon edges on map projections

I am trying to draw rectangles on different map projections using d3.js and geojson. The mapped coordinates seem right, however the edges appear curved in a strange way. I understand that this may have to do with the shortest path on the real Earth, but what I would like is that the edges follow the parallels/meridians graticule of the projection. Is there a way to do that? Can anyone help?
Example: Aitoff Projection
Example: Mercator
Here is the code I am using:
<!DOCTYPE html>
<html>
<head>
<title>World Map</title>
<meta charset="utf-8">
<script src="https://d3js.org/d3-geo-projection.v2.min.js"></script>
<script src="https://d3js.org/d3.v4.min.js"></script>
<script src="https://d3js.org/topojson.v2.min.js"></script>
<style>
path {
fill: red;
stroke: #000;
stroke-width: .1px;
}
.graticule {
fill: none;
stroke: #000;
stroke-width: .2px;
}
.foreground {
fill: none;
stroke: #333;
stroke-width: 1.2px;
}
</style>
</head>
<body>
<svg width="960" height="600"></svg>
<script>
const svg = d3.select("svg")
const myProjection = d3.geoMercator()
const path = d3.geoPath().projection(myProjection)
const graticule = d3.geoGraticule()
const geojson = {
"type": "FeatureCollection",
"features": [
{ "type": "Feature", "properties": {
"color": "blue"
},
"geometry": {
"type": "Polygon",
"coordinates": [[[-80.0, 50.0], [-20.0, 50.0], [-20.0, -10.0], [-80.0, -10.0], [-80.0, 50.0]]]
} }
]
}
function drawMap(err, world) {
if (err) throw err
svg.append("g")
.selectAll("path")
.data(topojson.feature(world, world.objects.land).features)
.enter().append("path")
.attr("d", path);
svg.append("path")
.datum(graticule)
.attr("class", "graticule")
.attr("d", path);
svg.append("path")
.datum(graticule.outline)
.attr("class", "foreground")
.attr("d", path);
svg.append("g")
.selectAll("path")
.data(geojson.features)
.enter().append("path")
.attr("d", path)
}
d3.json("https://unpkg.com/world-atlas#1.1.4/world/50m.json", drawMap)
</script>
</body>
</html>
Your assumption is right: d3 uses great circle distances to draw lines: this means that any path between two points using a d3 geoProjection and geoPath follows the shortest real world path between those two points. This means:
that the same path between two points aligns with other geographic points and features no matter the projection
that the anti-meridian can be accounted for
and the resulting map more accurately depicts lines.
To draw straight lines and/or lines that follow parallels (meridians are the shortest path between two points that fall on them - so paths follow this already, assuming an unrotated graticule) there are a few possibilities.
The easiest solution is to use a cylindrical projection like a Mercator to create a custom geoTransform. d3.geoTransforms do not use spherical geometry, unlike d3.geoProjections, instead they use Cartesian data. Consequently they don't sample along lines to create curved lines: this is unecessary when working with Cartesian data. This allows us to use spherical geometry for the geojson vertices within the geoTransform while still keeping straight lines on the map:
var transform = d3.geoTransform({
point: function(x, y) {
var projection = d3.geoMercator();
this.stream.point(...projection([x,y]));
}
});
As seen below:
var projection = d3.geoMercator();
var transform = d3.geoTransform({
point: function(x, y) {
var projection = d3.geoMercator();
this.stream.point(...projection([x,y]));
}
});
var color = ["steelblue","orange"]
var geojson = {type:"LineString",coordinates:[[-160,60],[30,45]]};
var geojson2 = {type:"Polygon",coordinates:[[[-160,60,],[-80,60],[-100,30],[-160,60]]]}
var svg = d3.select("body")
.append("svg")
.attr("width",960)
.attr("height",500);
svg.selectAll(null)
.data([projection,transform])
.enter()
.append("path")
.attr("d", function(d) {
return d3.geoPath().projection(d)(geojson)
})
.attr("fill","none")
.attr("stroke",function(d,i) { return color[i]; } )
.attr("stroke-width",1);
svg.selectAll(null)
.data([projection,transform])
.enter()
.append("path")
.attr("d", function(d) {
return d3.geoPath().projection(d)(geojson2)
})
.attr("fill","none")
.attr("stroke",function(d,i) { return color[i]; } )
.attr("stroke-width",2);
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
The orange lines use the transform, the blue lines use a plain Mercator
In some cases you could set the precision of the projection (which regulates the adaptive sampling) to some absurdly high number, this will work for some lines, but not others due to things like anti-meridian cutting:
var projection = d3.geoMercator().precision(1000000);
var transform = d3.geoTransform({
point: function(x, y) {
var projection = d3.geoMercator();
this.stream.point(...projection([x,y]));
}
});
var color = ["steelblue","orange"]
var geojson = {type:"LineString",coordinates:[[-160,60],[30,45]]};
var geojson2 = {type:"Polygon",coordinates:[[[-160,60,],[-80,60],[-100,30],[-160,60]]]}
var svg = d3.select("body")
.append("svg")
.attr("width",960)
.attr("height",500);
svg.selectAll(null)
.data([projection,transform])
.enter()
.append("path")
.attr("d", function(d) {
return d3.geoPath().projection(d)(geojson)
})
.attr("fill","none")
.attr("stroke",function(d,i) { return color[i]; } )
.attr("stroke-width",1);
svg.selectAll(null)
.data([projection,transform])
.enter()
.append("path")
.attr("d", function(d) {
return d3.geoPath().projection(d)(geojson2)
})
.attr("fill","none")
.attr("stroke",function(d,i) { return color[i]; } )
.attr("stroke-width",2);
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
Neither approach works if you want to draw lines that are aligned with parallels on a non-cylindrical projection. For a cylindrical projection parallels are straight. The above approaches will only create straight lines. If the parallels aren't projected straight, such as the Aitoff, the lines will not align with the graticule.
To have a line follow a parallel you will need to sample points along your paths because the projected parallels will not all be straight and parallels don't follow great circle distance. Therefore neither the default projection nor the method above will work in these instances.
When sampling you will need to treat the data as Cartesian - essentially using a cylindrical projection (Plate Carree) to have lines follow parallels.

How to remove node in D3 force layout?

I have a simple D3 simulation that looks like this
When I click the 'remove 1 and 4' button I want to remove nodes 1 and 4 from the simulation. The result, however, is the following:
Visually it seems to have removed 3 and 4 (not 1 and 4).
My code is below. Any ideas what I need to do to make this work correctly?
I'm assuming I've fundamentally misunderstood how updating nodes works in d3. Any help appreciated.
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
<!DOCTYPE html>
<meta charset="utf-8">
<script type="text/javascript" src="http://d3js.org/d3.v3.js"></script>
<body>
<button>remove 1 and 4</button>
<script>
var width = 400,
height = 400;
var nodes = [1, 2, 3, 4].map(function(x) { return { name: x}});
var force = d3.layout.force()
.size([width, height])
.nodes(nodes)
.linkDistance(30)
.charge(-500)
.on("tick", tick);
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height);
var content = svg.append("g");
var nodesData = force.nodes(),
nodeElement = content.selectAll(".node");
function tick() {
nodeElement.attr("transform", function(d) { return "translate(" + d.x + "," + d.y + ")"; });
}
d3.selectAll('button').on('click', function() {
//remove 1 and 4
nodesData = [
nodesData[1],
nodesData[2]
]
refresh();
});
var WIDTH = 100;
//
// this logic is slightly modified from http://bl.ocks.org/tgk/6068367
//
function refresh() {
force.nodes(nodesData)
nodeElement = nodeElement.data(nodesData);
var nodeGroup = nodeElement.enter()
.append('g')
.attr("class", "node");
// node text
nodeGroup.append("text")
.attr("class", "nodetext")
.attr("dx", WIDTH/2)
.attr("dy", "14px")
.text(function(n) { return 'node '+n.name })
nodeElement.exit().remove();
force.start();
}
refresh();
</script>
You can solve your problem by adding a "key" function to the .data call inside the refresh function: nodeElement = nodeElement.data(nodesData, function(d){ return d.name });.
The problem you saw is not specific to updating nodes. Ordinarily, selections work based off of index of the data array. So if first D3 had [a,b,c,d] and now it has [a,d], it's going to take the first two elements ([a,b]) unless you tell it the key that defines each item in the array. That's what the function above does.
For more see https://github.com/d3/d3-selection/blob/master/README.md#selection_data

D3.js v4 Force layout and fixed node anomaly

I have a D3.js V4 Force layout, comprising two nodes linked by a single edge. One node is fixed near top left, the other free to move.
When the layout is run, the non-fixed node starts in the middle of the layout, and moves away from the fixed node, as though repelled. It ends up in the opposite corner from the fixed node.
I would expect the free node to end up between the centre of the layout (where gravity pulls it) and the fixed node (pulled towards it by the link force). What am I missing, please?
var width = 240,
height = 150;
var nodes = [
{ // Larger node, fixed
fx: 20, fy: 20, r: 10
},
{ // Small node, free
r: 5
}];
var links = [{ // Link the two nodes
source: 0, target: 1
}
];
var svg = d3.select('body').append('svg')
.attr('width', width)
.attr('height', height);
// Create simulation with layout-centring and link forces
var force = d3.forceSimulation()
.force("centre", d3.forceCenter(width / 2, height / 2))
.force("link", d3.forceLink())
.nodes(nodes);
force.force("link").links(links);
// Draw stuff
var link = svg.selectAll('.link')
.data(links)
.enter().append('line')
.attr('class', 'link');
var node = svg.selectAll('.node')
.data(nodes)
.enter().append('circle')
.attr('class', 'node')
force.on('tick', function() {
node.attr('r', function(d) {
return d.r;
})
.attr('cx', function(d) {
return d.x;
})
.attr('cy', function(d) {
return d.y;
});
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;
});
});
<script src="https://d3js.org/d3.v4.min.js"></script>
<!DOCTYPE html>
<html>
<head>
<meta charset='utf-8'>
<style>
.node {
fill: #f00;
stroke: #fff;
stroke-width: 2px;
}
.link {
stroke: #777;
stroke-width: 2px;
}
</style>
</head>
<body>
</body>
</html>
The Centering force will try to have the center of mass of all nodes in the coordinate given. As you have 2 nodes and 1 is fixed, the other will be symmetric to the fixed node.
d3 Centering force documentation
Centering
The centering force translates nodes uniformly so that the mean
position of all nodes (the center of mass if all nodes have equal
weight) is at the given position ⟨x,y⟩.

Dimple JS add vertical line

I am trying to draw a vertical line in Dimple. I have seen this post: How to draw a vertical line with dimple?
However, the example provided there is not working for me. It is only adding a point where instead I'd like a line to be drawn. I cannot find anything in the docs, nor on StackOverflow, nor via Googling and I would think this should be an easy thing to do- but so far it has proven not to be. Any help would be appreciated.
Fiddle: http://jsfiddle.net/4w6gkq5s/27/
<!DOCTYPE html>
<html>
<body>
<div id="chartContainer">
<script src="http://dimplejs.org/lib/d3.v3.4.8.js"></script>
<script src="http://dimplejs.org/dist/dimple.v2.1.2.min.js"></script>
<script type="text/javascript">
var svg = dimple.newSvg("#chartContainer", 590, 400);
d3.tsv("http://dimplejs.org/data/example_data.tsv", function (data123) {
var data = [{'name': "name1", "percentChange": 120}, {'name': "name2", "percentChange": 80}, {'name': "name3", "percentChange": 100}];
var myChart = new dimple.chart(svg, data);
myChart.defaultColors = [ new dimple.color("#177c5f") ];
myChart.defaultColors = [
new dimple.color("#3d5739"), // green
];
var x2 = myChart.addMeasureAxis("x", "percentChange");
x2.title = "Percent of last month's sales"
var y = myChart.addCategoryAxis("y", "name");
y.addOrderRule("name");
y.title = null;
y.showGridLines = true;
/*Regional average*/
var y2 = myChart.addPctAxis("y", "Dummy");
var s3 = myChart.addSeries("Regional", dimple.plot.area, [x2, y2]);
s3.data = [{
"Regional": "Regional",
"Dummy": 1,
"percentChange": 105
}];
var s = myChart.addSeries(["percentChange", "name"], dimple.plot.bar);
s.barGap = .86;
// Set some custom display elements for each series shape
s.afterDraw = function (shape, data) {
var shape = d3.select(shape);
// Add some bar labels for the yValue
svg.append("text")
.attr("x", parseFloat(shape.attr("x")) + 40)
.attr("y", parseFloat(shape.attr("y")) - 5)
.style("text-anchor", "middle")
.style("font-size", "16px")
.style("fill", "#73b7e8")
.style("pointer-events", "none")
.text(data.cy);
};
s.addEventHandler("mouseover", onHover);
s.addEventHandler("mouseleave", onLeave);
myChart.draw();
function onHover(e) {
e.selectedShape[0][0].style.fill = "#00924f";
};
function onLeave(e) {
e.selectedShape[0][0].style.fill = "#3d5739";
};
});
</script>
</div>
</body>
</html>
That approach won't work with a measure axis on x. However the solution is actually much simpler in this case. After drawing you can add a line with a bit of d3:
svg.append("line")
.attr("x1", x._scale(105))
.attr("x2", x._scale(105))
.attr("y1", myChart._yPixels())
.attr("y2", myChart._yPixels() + myChart._heightPixels())
.style("stroke", "red")
.style("stroke-dasharray", "3");
Fiddle: http://jsfiddle.net/d3jo0uu5/1/
This uses a couple of the internal methods in dimple for height and width as well as the x axis scale for positioning the line horizontally. I've made it red and dashed but changing the style lines you can format it however you wish - or set a class instead and set the appearance in css.

Issues with d3 zoomable map

I'm having an issue with a d3 zoomable map.
I'm loading the map from a previously built topojson file with a departmentsobject (the areas in the map) and a maternidadesobject (a few points in the map, initially rendered with crosses).
I'm using d3.behavior.zoom to implement the zoom behaviour, and I want it to be able to zoom using the mouse wheel and pan with drag. It works just fine with the map itself (the areas). However, the points in the map get shifted instantly to a wrong location at any zoom event. Also, the points' path is changed from crosses to circles somehow!
You can reproduce the issue and view the code here: http://bl.ocks.org/monsieurBelbo/5033491
Here is code:
<!DOCTYPE html>
<meta charset="utf-8">
<script src="http://d3js.org/d3.v3.js"></script>
<script src="topojson.v0.min.js"></script>
<html>
<style>
.background {
fill: none;
pointer-events: all;
}
.department {
fill: #aaa;
stroke: #fff;
stroke-width: 1.5px;
}
</style>
<body>
<script>
d3.json("santafe.json", function(error, theProvince) {
var width= 960, height= 500;
var svg = d3.select("body").append("svg");
var departments = topojson.object(theProvince, theProvince.objects.departments);
// The projection
var projection = d3.geo.mercator()
.scale(14000)
.center([-60.951,-31.2])
.translate([width / 2, height / 2]);
// The path
var path = d3.geo.path()
.projection(projection);
// Zoom behavior
var zoom = d3.behavior.zoom()
.translate(projection.translate())
.scaleExtent([height, Infinity])
.scale(projection.scale())
.on("zoom", function() {
projection.translate(d3.event.translate).scale(d3.event.scale)
map.selectAll("path.zoomable").attr("d", path);
});
// The map
var map = svg.append("g")
.classed("provinceMap", true)
.call(zoom);
map.append("rect")
.attr("class", "background")
.attr("width", width)
.attr("height", height);
// Departments
map.selectAll(".department")
.data(departments.geometries)
.enter().append("path")
.classed("department", true)
.classed("zoomable", true)
.attr("d", path);
// Places
map.selectAll(".place-label")
.data(topojson.object(theProvince, theProvince.objects.maternidades).geometries)
.enter().append("path")
.classed("place", true)
.classed("zoomable", true)
.attr("d", d3.svg.symbol().type("cross"))
.attr("transform", function(d) { return "translate(" + projection(d.coordinates.reverse()) + ")"; });
});
</script>
</body>
</html>
Any ideas?
Thanks!
UPDATE
Thanks to #enjalot 's suggestion, the issue was solved by re-translating the places on the zoom behaviour. Just add:
map.selectAll(".place").attr("transform", function(d) { return "translate(" + projection(d.coordinates) + ")"; });
to the zoom behavior. Check out a working version here: http://tributary.io/inlet/5095947

Resources