D3 centre point of a line (not path) - javascript

are a few questions on here and on the D3 site about how you'd find the centre point (or any point) along a path, however I can't seem to find how to do it with a line.
I've done a simple jsfiddle here. Essentially I need to add a shape (using text in the jsfiddle to make it clearer) at a point along a line (lets say the middle for simplicity)
So I have a svg:
var canvas = d3.select('body').append('svg').attr('width', 500).attr('height', 500);
And add a line (the position is fixed and doesnt come from data)
var line = canvas.append('line').attr('x1', 50).attr('y1', 50).attr('x2', 250).attr('y2', 150);
The I add some text just to demo to the top and bottom of that line
canvas.append('text').attr('x', line.attr('x1')).attr('y', line.attr('y1')).text('top');
canvas.append('text').attr('x', line.attr('x2')).attr('y', line.attr('y2')).text('bottom');
path's have methods to get the centre point and width/BBox etc, but line doesnt seem to.
Anyone have any ideas how this can be achieved?
My initial though was to just get the difference between the x1/x2 values, like this:
canvas.append('text')
.attr('x', parseInt(line.attr('x2') - line.attr('x1')))
.attr('y', parseInt(line.attr('y2') - line.attr('y1')))
.text('just looks a bit off');
But as you'll see from the jsfiddle, it's just off somehow.
Anyone want to point out my mistake?

I guess, this will work:
var lineData = {x1: 50, y1: 50, x2: 250, y2: 150};
var canvas = d3.select('body').append('svg').attr('width', 500).attr('height', 500);
var line = canvas.append('line').attr('x1', lineData.x1).attr('y1', lineData.y1).attr('x2', lineData.x2).attr('y2', lineData.y2);
console.log(line);
var x = lineData.x1 + Math.abs(lineData.x2 - lineData.x1) / 2;
var y = lineData.y1 + Math.abs(lineData.y2 - lineData.y1) / 2;
console.log([x,y]);
canvas.append('text').attr('x', x).attr('y', y).text('X');

Line
Use simple mathematics, distance formula.
var canvas = d3.select('body').append('svg').attr('width', 500).attr('height', 500);
var line = canvas.append('line').attr('x1', 50).attr('y1', 50).attr('x2', 250).attr('y2', 150);
var x1 = parseInt(line.attr("x1"));
var y1 = parseInt(line.attr("y1"));
var x2 = parseInt(line.attr("x2"));
var y2 = parseInt(line.attr("y2"));
var midPoint = { x: (x1+x2)/2, y: (y1+y2)/2 };
canvas.append('text').attr('x', midPoint.x).attr('y', midPoint.y).text('X');
line{
stroke:#444;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
Path
var canvas = d3.select('body').append('svg').attr('width', 500).attr('height', 500);
var line = canvas.append('path').attr('d', "M 50 50 L 250 150");
var path = line.node();
var midPoint = path.getPointAtLength(path.getTotalLength()/2);
canvas.append('text').attr('x', midPoint.x).attr('y', midPoint.y).text('X');
path{
stroke: red;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>

Resolved the issue so thought I'd answer it incase anyone else found the question.
Used getBBox() of the node (using .node()). Used that to get the width, height and xy:
var canvas = d3.select('body').append('svg')
.attr('width', 500)
.attr('height', 500);
var line = canvas.append('line')
.attr('x1', 50)
.attr('y1', 150)
.attr('x2', 50)
.attr('y2', 250);
Then the middle x and y are:
var midX = line.node().getBBox().x + line.node().getBBox().width / 2;
var midY = line.node().getBBox().y + line.node().getBBox().height / 2;

Related

How to properly apply a color gradient to an arc?

I am working on building a visual that looks something like this: .
So far I've managed to create this:
The idea is to map a value to an angle so that I know where to point the arrow and then I will color the arrow the same color as the point on the arc that its pointing to.
I essentially have two questions:
First what can I do in order to make the colors line up better. I've used a linear gradient like so:
let defs = this.gaugeEl
.append("defs")
.classed("definitions",true);
let gradient = defs
.append("linearGradient")
.classed("linearGradient",true);
gradient
.attr({
id: 'gradient',
x1: '0%',
y1: '0%',
x2: '100%',
y2: '100%',
spreadMethod: "pad"
});
gradient
.append("stop")
.classed('start',true)
.attr({
offset: '0%',
'stop-color': 'lawngreen',
'stop-opacity': 1
});
gradient.append("stop")
.classed('end',true)
.attr({
offset: '100%',
'stop-color': 'red',
'stop-opacity': 1
});
The effect is not what I was hoping for, what can be done?
The next question about how the gradient works, I need to be able to associate an angle with a color so that I can color the arrow and the tick marks properly and in my current setup I don't know how to do that. Is it even possible?
I don't know how much useful this will be for you. But I followed the below implementation
Split the arc into tiny arcs
Used scaleLinear for associating color and angle and divided the arc into four segments
Ignore bad math and code !
<!DOCTYPE html>
<meta charset="utf-8">
<style>
#chart {
width: 960px;
height: 350px;
}
</style>
<body>
<svg id="chart">
</svg>
<script src="http://d3js.org/d3.v5.min.js"></script>
<script>
var vis = d3.select("#chart").append("g")
var pi = Math.PI;
var line = d3.line()
.x(function (d) { return d.x; })
.y(function (d) { return d.y; });
var lines = []
var breakPoints = 100;
var angleArr = [];
var arcArr = [];
//angleArr[0] = -pi/2;
var colorScale = d3.scaleLinear()
.domain([-pi/2, -pi/3,30*pi/180,pi/2])
.range(['lightgreen', 'lightgreen', 'yellow','red']);
var angleScale = d3.scaleLinear()
.range([-pi/2,pi/2])
.domain([0,breakPoints - 1]);
var prevAngle = -pi/2;
for(var i = 0; i < breakPoints; i++) {
angleArr[i] = angleScale(i);
var singleArrow = [{"x":(150*Math.sin(angleArr[i])), "y":-(150*Math.cos(angleArr[i]))},{ "y":-(170*Math.cos(angleArr[i])), "x":(170*Math.sin(angleArr[i]))}];
//var subArc = {"start": prev, "end":0};
var subArc = {};
lines.push(singleArrow);
subArc["start"] = prevAngle;
subArc["end"] = angleArr[i];
prevAngle = angleArr[i];
arcArr.push(subArc);
}
var arc = d3.arc()
.innerRadius(160)
.outerRadius(170)
.startAngle(-(pi/2)) //converting from degs to radians
.endAngle(pi/2) //just radians
vis.attr("width", "400").attr("height", "400") // Added height and width so arc is visible
.append("g")
.attr("transform", "translate(200,200)");
vis.selectAll("line")
.data(lines)
.enter()
.append("path").attr("class","arrow").attr("d", line).attr("stroke",function(d,i) {
return colorScale(angleArr[i])}).attr("transform", "translate(200,200)");
vis.selectAll("arcs")
.data(arcArr)
.enter()
.append("path").attr("class","arc").attr("d", function(d,i) {
return d3.arc()
.innerRadius(160)
.outerRadius(170)
.startAngle(d.end)
.endAngle(d.start)()}).attr("fill",function(d,i) {
return colorScale(angleArr[i])}).attr("transform", "translate(200,200)");
</script>
</body>
There is an easier way to do this now with a css property called conic-gradient
https://css-tricks.com/snippets/css/css-conic-gradient/
It sets the color according to the angle, given a center point. Maybe you could get the angle to the point with a click event, calculate the angel from the center, and set the colour that way.
There's some more info on conic gradients here, including how to calculate it: https://wiki.inkscape.org/wiki/index.php/Advanced_Gradients#Conical_gradient

Connected two line using svg in javascript

I draw two line using svg, but when I render it, I only view one line and the other can't be seen. I dont know how to append that two line and make it all seen. That two line supposed to be seen when I click a polygon. Can anyone help me?
Here's my code
var group = evt.target.parentNode;
// Get the bounding box of the group
var bbox = group.getBBox();
// Add a triangle to the group
var svgns = "http://www.w3.org/2000/svg";
var line = document.createElementNS(svgns, "line");
var line2 = document.createElementNS(svgns, "line");
line.setAttribute('id','line2');
line.setAttribute('x1','0');
line.setAttribute('y1','0');
line.setAttribute('x2','5');
line.setAttribute('y2','19');
line.setAttribute("stroke", "black")
line2.setAttribute('id','line2');
line2.setAttribute('x1', '7');
line2.setAttribute('y1','5');
line2.setAttribute('x2','5');
line2.setAttribute('y2','19');
line2.setAttribute("stroke", "black");
var xPos = bbox.x + bbox.width / 2;
var yPos = bbox.y + bbox.height / 2;
line2.setAttribute("transform", "translate(" + xPos + "," + yPos + ")");
group.appendChild(line2);
I see you are appending line2 to the group.
group.appendChild(line2);
But where is the appendChild() call for the other line?

How to move along a path but only between two specified path points

This question is about d3 version 3.x and path movements.
Imagine there is a path and a circle element, and I want the circle to follow that path in a transition, but only up to some percentage.
I was asking this before and got a great answer from Gerardo Furtado here: My former question
Still, one question in this regards remains for me, and as I am a beginner, I couldn't find any working solution so far:
How can i trace this path, lets say, from a point at 25% to a point at 50%, and then later from a point at 50% to a point at 60%?
These numbers are just examples, any percentage value should be possible.
I need to avoid that the path movement always starts from position 0, but instead i want to start path movement from the current position the circle has reached already.
Hopefully I could express my question clear enough.
Thank you very much for any insight and help.
Well, I have to agree with LeBeau, and I see that you also do. Since I answered your last question, I just needed to do some minor changes in the function. However, keep his advise in mind for the next times: when asking a question, show us some code you've tried, even if it doesn't work, because it shows effort.
Back to the question.
For this solution, I'll wrap everything inside a function named move, which accepts two arguments, the initial position and the final position (both in percentages):
function move(initialPosition, finalPosition) {
The initial position, as the name implies, set the initial position of the circle along the path. The math is this:
var start = path.node()
.getPointAtLength(path.node().getTotalLength() * initialPosition);
Then, I slightly changed the function from my last answer to accept the initial and final positions:
function translateAlong(path) {
var l = path.getTotalLength() * (finalPosition - initialPosition);
return function() {
return function(t) {
var p = path.getPointAtLength(t * l +
(path.getTotalLength() * initialPosition));
return "translate(" + p.x + "," + p.y + ")";
};
};
}
Here is the demo. Clicking on the button calls move with 0.25 (initial position) and 0.5 (final position) as arguments:
var points = [
[240, 100],
[290, 200],
[340, 50],
[390, 150],
[90, 150],
[140, 50],
[190, 200]
];
var svg = d3.select("body").append("svg")
.attr("width", 500)
.attr("height", 300);
var path = svg.append("path")
.data([points])
.attr("d", d3.svg.line()
.tension(0) // Catmull–Rom
.interpolate("cardinal-closed"));
var color = d3.scale.category10();
var dataPositions = [{
initial: 0.25,
final: 0.5
}, {
initial: 0.5,
final: 0.6
}];
svg.selectAll(".point")
.data(points)
.enter().append("circle")
.attr("r", 4)
.attr("transform", function(d) {
return "translate(" + d + ")";
});
d3.select("button").on("click", function() {
move(0.25, 0.5);
});
function move(initialPosition, finalPosition) {
var start = path.node().getPointAtLength(path.node().getTotalLength() * initialPosition);
var circle = svg.append("circle")
.attr("r", 13)
.attr("fill", function(d, i) {
return color(i)
})
.attr("transform", "translate(" + start.x + "," + start.y + ")");
circle.transition()
.duration(1000)
.attrTween("transform", function() {
return translateAlong(path.node())()
});
function translateAlong(path) {
var l = path.getTotalLength() * (finalPosition - initialPosition);
return function() {
return function(t) {
var p = path.getPointAtLength(t * l +
(path.getTotalLength() * initialPosition));
return "translate(" + p.x + "," + p.y + ")";
};
};
}
}
path {
fill: none;
stroke: #000;
stroke-width: 3px;
}
circle {
stroke: #fff;
stroke-width: 3px;
}
<script src="//d3js.org/d3.v3.min.js"></script>
<button>Move</button>
<br>
PS: The function in this answer does not accept values bigger than 1. But you can try to change it if you need to do more than "one lap" in the path.

D3js canvas and line are not visible

I have the following code which should to display drowned line in the canvas element.
var initCanvas = function () {
var episodeLengthInPixels = moment.duration(episodeLogLength).asSeconds() * episodeWidthMultiplication;
console.log("Length of chart is "+episodeLengthInPixels +" px");
try {
canvas = d3.select("body").append("canvas")
.attr("width", 500)
.attr("height", canvasHeight)
.attr("class", canvasSelector);
//Draw the Line
canvas.append("line") // attach a line
.style("stroke", "black") // colour the line
.attr("x1", 0) // x position of the first end of the line
.attr("x2", 500)
.attr("y1", waveHeight)
.attr("y2", waveHeight) ;
} catch (e) {
console.error(e);
}
}
Problem is that canvas and the line are available in the DOM model but are not visible (no exception is throwned). When i tried to work with SVG instead of the canvas, everything works fine.
How can I display the content in canvas using the D3.js library please? I tried to find any examples, but without the luck. Should i use D3.js fro canvas usage or something else (pure drawing to canvas in example)?
Many thanks for any advice.
Canvas and SVG are way different. It's not just a matter of changing "svg" for "canvas" in your d3.select("body").append() code. You should study the canvas documentation and the SVG documentation.
This, for instance, is how to draw a line in canvas:
var chart = d3.select("body").append("canvas")
.attr("width", 400)
.attr("height", 300);
var context = chart.node().getContext("2d");
context.beginPath();
context.moveTo(0,100);//here you set the equiv. to X1 and Y1 in SVG
context.lineTo(400,100);//here you set the equiv. to X2 and Y2 in SVG
context.stroke();
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
Also, keep in mind that the fact that you see a given element when inspecting the DOM doesn't mean that the element will show up. You can make this very simple test using d3:
d3.select("body").append("div").append("charlesdarwin");
You're gonna see this inspecting the DOM:
<div>
<charlesdarwin></charlesdarwin>
</div>
But, of course, you don't expect that this have any result.
Here is kinda an example taken from here.
https://bocoup.com/weblog/d3js-and-canvas
d3 and canvas are not the same.
var base = d3.select("#foo");
var chart = base.append("canvas")
.attr("width", 400)
.attr("height", 300);
var context = chart.node().getContext("2d");
var data = [1,2,13,20,23];
var scale = d3.scale.linear()
.range([10, 390])
.domain([1,23]);
data.forEach(function(d, i) {
context.beginPath();
context.rect(scale(d), 150, 10, 10);
context.fillStyle="red";
context.fill();
context.closePath();
});
// Your line here...
context.beginPath();
context.moveTo(10,10);
context.lineTo(40,60); // x2,y2 ...
context.stroke();
context.closePath();
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
Examples here
<div id="foo"></div>

Rotating Globe Choropleth in D3

I'm trying to make a chorolpleth with a rotating globe in d3. I can get the globe to render out fine but I can't get the countries to fill with the proper color scale.
Longer explanation. I basically started with Mike Bostock's code for the spinning globe found here:
http://bl.ocks.org/mbostock/6747043
I've got some economic data for about 85 countries that I'm reading in from an external csv. And I'm trying to get the colors to map to the countries per the values in the csv. There's another Bostock example of a choropleth here (static and just the US and referenced frequently in SO d3 questions):
http://bl.ocks.org/mbostock/4060606
What I end up with are solid white (#fff) countries on the face of the globe. Which is not what I'm trying to get.
I added the ISO 3166-1 numeric codes to my csv so that I could match them to the same ids inside the topojson data. So my csv looks like:
country id curracct
Germany 276 260.9
Sweden 752 7.24
Etc.
My first thought was just to create a variable that was a function, which went through the length of the 'countries' from the topojson data and found the countries where the id equaled the id from the csv countries, then assigned the scaled color to them. Then I set 'context.fillStyle' equal to that variable/function. That didn't work.
Then I just put 'context.fillStyle' directly inside of a function (which is the code as it's currently written below). That didn't work either.
Again, I'm trying to get the 85 or so countries with data in the csv to appear color-coded on the front side spinning globe according to the scale I've set up.
My guess is that there is something I don't understand about the variable 'context' and what it's handling. If this were .style("fill", [put my function here to map the colors]) syntax I would be okay. So, anyone got any thoughts?
I'm not a coder. Actually I guess I am as I am trying to write some code. Maybe I should just say I'm a self-taught and mostly terrible coder. Although through examples, the JS console, and other questions on SO, I can usually work out where the errors are. This time I've reached a wall. Any help is appreciated. Thanks.
var width = 560,
height = 560,
speed = -1e-2,
start = Date.now();
var sphere = {type: "Sphere"};
var color = d3.scale.quantize()
.range(["#ffffd9", "#edf8b1","#c7e9b4","#7fcdbb","#41b6c4","#1d91c0","#225ea8","#253494","#081d58"]);
var projection = d3.geo.orthographic()
.scale(width / 2.1)
.clipAngle(90)
.translate([width / 2, height / 2]);
var graticule = d3.geo.graticule();
var canvas = d3.select("body")
.append("canvas")
.attr("width", width)
.attr("height", height);
var context = canvas.node().getContext("2d");
var path = d3.geo.path()
.projection(projection)
.context(context);
queue()
.defer(d3.json, "/d3/world-110m.json")
.defer(d3.csv, "trade.csv")
.await(globeTrade);
function globeTrade(error, topo, data) {
var land = topojson.feature(topo, topo.objects.land),
countries = topojson.feature(topo, topo.objects.countries),
borders = topojson.mesh(topo, topo.objects.countries, function(a, b) { return a !== b; }),
grid = graticule();
color.domain([0, d3.max(data, function(d){return d.curracct})]);
d3.timer(function() {
var λ = speed * (Date.now() - start),
φ = -15;
context.clearRect(0, 10, width, height);
context.beginPath();
path(sphere);
context.lineWidth = 2.5;
context.strokeStyle = "#000";
context.stroke();
context.fillStyle = "#fff";
context.fill();
context.save();
context.translate(width / 2, 0);
context.scale(-1, 1);
context.translate(-width / 2, 0);
projection.rotate([λ + 180, -φ]);
context.beginPath();
path(land);
context.fillStyle = "#ddd" //changed to a nuetral gray
context.fill();
context.beginPath();
path(grid);
context.lineWidth = .5;
context.strokeStyle = "rgba(119,119,119,.5)";
context.stroke();
context.beginPath();
path(borders);
context.lineWidth = .25;
context.strokeStyle="#fff";
context.stroke();
context.restore();
projection.rotate([λ, φ]);
context.beginPath();
path(grid);
context.lineWidth = .5;
context.strokeStyle = "rgba(119,119,119,.5)";
context.stroke();
// This is where I am failing
context.beginPath();
path(countries);
function render (d){
for (var j = 0; j < countries.features.length; j++) {
if (d.id == countries.features[j].id) {
context.fillStyle = color(d.curracct)
}
else {
context.fillStyle = "#737368"; //left Bostock's color for now
}
}
}
context.fill();
context.lineWidth = .1;
context.strokeStyle = "#000";
context.stroke();
});
data.forEach(function(d, i) {
d.curracct = +d.curracct;
d.id = +d.id;
});
d3.select(self.frameElement).style("height", height + "px");
</script>
</body>

Categories

Resources