Stopping a Line At the Outer Edge of a Circle - javascript

Hey guys I recently started learning D3.js and have ran into a problem: http://i.stack.imgur.com/Nqghl.png. How can I stop drawing the line at the outer edge of these circles?
Possibly another solution could be to re-arrange the layers so that the circle one is on top of the line one.
Here is my sample code
data = [{name: 'one', parent: 'one', a: 1}, {name: 'two', parent: 'one', a: 2}, {name: 'three', parent: 'one', a: 2}]
r = 30;
var centerX = function (d, i) {
return (i * ((r * 2) + 20)) + r;
};
var centerY = function (a, i) {
return (a * 160) + (r * 2);
}
var global = d3.select('body')
.append('svg')
.attr('width', 500)
.attr('height', 500)
global.selectAll("circle")
.data(data)
.enter()
.append("circle")
.style("stroke", "gray")
.style("fill", "aliceblue")
.attr('r', r)
.attr('cx', function(d, i ) {return centerX(r, i)})
.attr('cy', function(d, i) {return centerY(d.a, i)})
.attr('id', function(d) { return 'one'});
global.selectAll("line")
.data(data)
.enter()
.append("line")
.style("stroke", "rgb(6,120,155)")
.style("stroke-width", 4)
.style('stroke-opacity', .4)
.attr('x1', function(d, i) {return centerX(r, i)})
.attr('y1', function(d, i) {return centerY(d.a, i)})
.attr('x2', function(d) {
var selector = "[id="+d.parent+"]";
return global.select(selector).attr('cx');
})
.attr('y2', function(d) {
var selector = "[id="+d.parent+"]";
return global.select(selector).attr('cy');
})
Any ideas? Thanks in advance!

You are right about switching the order in which you append the lines and circles, lines first, then circles. You just have to be careful to preserve the line selection in a variable that you can use to apply the line attributes that are dependent on the circles AFTER drawing the circles.
FIDDLE example
lines
.attr('x2', function(d) {
var selector = "[id="+d.parent+"]";
return global.select(selector).attr('cx');
})
.attr('y2', function(d) {
var selector = "[id="+d.parent+"]";
return global.select(selector).attr('cy');
});

Related

Resetting drag with multiple elements

I have multiple svg groups (each containing a circle and text) which I am dragging via d3-drag from an initial position. I have a rectangular hit zone that I only want one of these draggable groups in at a time. So whenever two groups are in the hit zone, I would like the first group that was in the hit zone to fade away and reappear in its initial position.
I have tried doing this via a function which translates the group back to its initial position by finding the current position of the circle shape and translating like:
translate(${-current_x}, ${-current_y})
This does translate the group back to the (0,0) position, so I have to offset by its initial position. I do this by setting the initial x and y values of the circle shape as attributes in the circle element and incorporating these into the translation:
translate(${-current_x + initial_x}, ${-current_y + initial_y})
Here is a block of my attempt:
https://bl.ocks.org/interwebjill/fb9b0d648df769ed72aeb2755d3ff7d5
And here it is in snippet form:
const circleRadius = 40;
const variables = ['one', 'two', 'three', 'four'];
const inZone = [];
// DOM elements
const svg = d3.select("body").append("svg")
.attr("width", 960)
.attr("height", 500)
const dragDockGroup = svg.append('g')
.attr('id', 'draggables-dock');
const dock = dragDockGroup.selectAll('g')
.data(variables)
.enter().append("g")
.attr("id", (d, i) => `dock-${variables[i]}`);
dock.append("circle")
.attr("cx", (d, i) => circleRadius * (2 * i + 1))
.attr("cy", circleRadius)
.attr("r", circleRadius)
.style("stroke", "none")
.style("fill", "palegoldenrod");
dock.append("text")
.attr("x", (d, i) => circleRadius * (2 * i + 1))
.attr("y", circleRadius)
.attr("text-anchor", "middle")
.style("fill", "white")
.text((d, i) => variables[i]);
const draggablesGroup = svg.append('g')
.attr('id', 'draggables');
const draggables = draggablesGroup.selectAll('g')
.data(variables)
.enter().append("g")
.attr("id", (d, i) => variables[i])
.call(d3.drag()
.on("start", dragStarted)
.on("drag", dragged)
.on("end", dragEnded));
draggables.append('circle')
.attr("cx", (d, i) => circleRadius * (2 * i + 1))
.attr("cy", circleRadius)
.attr("initial_x", (d, i) => circleRadius * (2 * i + 1))
.attr("initial_y", circleRadius)
.attr("r", circleRadius)
.style("stroke", "orange")
.style("fill", "yellowgreen");
draggables.append("text")
.attr("x", (d, i) => circleRadius * (2 * i + 1))
.attr("y", circleRadius)
.attr("text-anchor", "middle")
.style("fill", "white")
.text((d, i) => variables[i]);
svg.append('rect')
.attr("x", 960/2)
.attr("y", 0)
.attr("width", 100)
.attr("height", 500/2)
.attr("fill-opacity", 0)
.style("stroke", "#848276")
.attr("id", "hitZone");
// functions
function dragStarted() {
d3.select(this).raise().classed("active", true);
}
function dragged() {
d3.select(this).select("text").attr("x", d3.event.x).attr("y", d3.event.y);
d3.select(this).select("circle").attr("cx", d3.event.x).attr("cy", d3.event.y);
}
function dragEnded() {
d3.select(this).classed("active", false);
d3.select(this).lower();
let hit = d3.select(document.elementFromPoint(d3.event.sourceEvent.clientX, d3.event.sourceEvent.clientY)).attr("id");
if (hit == "hitZone") {
inZone.push(this.id);
if (inZone.length > 1) {
let resetVar = inZone.shift();
resetCircle(resetVar);
}
}
d3.select(this).raise();
}
function resetCircle(resetVar) {
let current_x = d3.select(`#${resetVar}`)
.select('circle')
.attr('cx');
let current_y = d3.select(`#${resetVar}`)
.select('circle')
.attr('cy');
let initial_x = d3.select(`#${resetVar}`)
.select('circle')
.attr('initial_x');
let initial_y = d3.select(`#${resetVar}`)
.select('circle')
.attr('initial_y');
d3.select(`#${resetVar}`)
.transition()
.duration(2000)
.style('opacity', 0)
.transition()
.duration(2000)
.attr('transform', `translate(${-current_x}, ${-current_y})`)
.transition()
.duration(2000)
.style('opacity', 1);
}
body { margin:0;position:fixed;top:0;right:0;bottom:0;left:0; }
<script src="https://d3js.org/d3.v5.min.js"></script>
Here are the problems:
While using translate(${-current_x}, ${-current_y}) works, when I try using translate(${-current_x + initial_x}, ${-current_y + initial_y}), the translation uses very large negative numbers (for example, translate(-52640, -4640)).
While using translate(${-current_x}, ${-current_y}) works, when I try to drag this translated group again, the group immediately repeats the previous translate(${-current_x}, ${-current_y})
Your code runs into difficulties because you are positioning both the g elements and the children text and circles.
Circles and text are originally positioned by x/y attributes:
draggables.append('circle')
.attr("cx", (d, i) => circleRadius * (2 * i + 1))
.attr("cy", circleRadius)
draggables.append("text")
.attr("x", (d, i) => circleRadius * (2 * i + 1))
.attr("y", circleRadius)
Drag events move the circles and text here:
d3.select(this).select("text").attr("x", d3.event.x).attr("y", d3.event.y);
d3.select(this).select("circle").attr("cx", d3.event.x).attr("cy", d3.event.y);
And then we reset the circles and text by trying to offset the parent g with a transform:
d3.select(`#${resetVar}`).attr('transform', `translate(${-current_x}, ${-current_y})`)
Where current_x and current_y are the current x,y values for the circles and text. We have also stored the initial x,y values for the text, but altogether, this becomes a more convoluted then necessary as we have two competing sets of positioning coordinates.
This can be simplified a fair amount. Instead of positioning both the text and the circles, simply apply a transform to the parent g holding both the circle and the text. Then when we drag we update the transform, and when we finish, we reset the transform.
Now we have no modification of x,y/cx,cy attributes and transforms for positioning the elements relative to one another. No offsets and the parent g's transform will always represent the position of the circle and the text.
Below I keep track of the original transform with the datum (not an element attribute) - normally I would use a property of the datum, but you have non-object data, so I just replace the datum with the original transform:
const circleRadius = 40;
const variables = ['one', 'two', 'three', 'four'];
const inZone = [];
// DOM elements
const svg = d3.select("body").append("svg")
.attr("width", 960)
.attr("height", 500)
const dragDockGroup = svg.append('g')
.attr('id', 'draggables-dock');
// Immovable placemarkers:
const dock = dragDockGroup.selectAll('g')
.data(variables)
.enter().append("g")
.attr("id", (d, i) => `dock-${variables[i]}`);
dock.append("circle")
.attr("cx", (d, i) => circleRadius * (2 * i + 1))
.attr("cy", circleRadius)
.attr("r", circleRadius)
.style("stroke", "none")
.style("fill", "palegoldenrod");
dock.append("text")
.attr("x", (d, i) => circleRadius * (2 * i + 1))
.attr("y", circleRadius)
.attr("text-anchor", "middle")
.style("fill", "white")
.text((d, i) => variables[i]);
// Dragables
const draggablesGroup = svg.append('g')
.attr('id', 'draggables');
const draggables = draggablesGroup.selectAll('g')
.data(variables)
.enter()
.append("g")
.datum(function(d,i) {
return "translate("+[circleRadius * (2 * i + 1),circleRadius]+")";
})
.attr("transform", (d,i) => d)
.attr("id", (d, i) => variables[i])
.call(d3.drag()
.on("start", dragStarted)
.on("drag", dragged)
.on("end", dragEnded));
draggables.append('circle')
.attr("r", circleRadius)
.style("stroke", "orange")
.style("fill", "yellowgreen");
draggables.append("text")
.attr("text-anchor", "middle")
.style("fill", "white")
.text((d, i) => variables[i]);
svg.append('rect')
.attr("x", 960/2)
.attr("y", 0)
.attr("width", 100)
.attr("height", 500/2)
.attr("fill-opacity", 0)
.style("stroke", "#848276")
.attr("id", "hitZone");
// functions
function dragStarted() {
d3.select(this).raise();
}
function dragged() {
d3.select(this).attr("transform","translate("+[d3.event.x,d3.event.y]+")")
}
function dragEnded() {
d3.select(this).lower();
let hit = d3.select(document.elementFromPoint(d3.event.sourceEvent.clientX, d3.event.sourceEvent.clientY)).attr("id");
if (hit == "hitZone") {
inZone.push(this.id);
if (inZone.length > 1) {
let resetVar = inZone.shift();
resetCircle(resetVar);
}
}
d3.select(this).raise();
}
function resetCircle(resetVar) {
d3.select(`#${resetVar}`)
.transition()
.duration(500)
.style('opacity', 0)
.transition()
.duration(500)
.attr("transform", (d,i) => d)
.transition()
.duration(500)
.style('opacity', 1);
}
body { margin:0;position:fixed;top:0;right:0;bottom:0;left:0; }
<script src="https://d3js.org/d3.v5.min.js"></script>

d3: repeat transition for series of elements?

I'm having trouble getting a transition to repeat, for a series of elements, in this case a set of three lines. The animation runs just fine once, but when it is repeated (with the same data), all three lines merge into a single line (the last array in data). What am I doing wrong?
(function() {
var w = 100, h = 100
var div = d3.select('#sketches').append('div')
var svg = div.append("svg")
.attr("width", w)
.attr("height", h)
var x = 0, y = 55
var data = [
[x, y, x+20, y-40],
[x+10, y, x+30, y-40],
[x+20, y, x+40, y-40]
];
(function lines() {
svg.selectAll('line')
.data(data).enter().append('line')
.attr("stroke", "black")
.attr('x1', function(d) {return d[0]})
.attr('y1', function(d) {return d[1]})
.attr('x2', function(d) {return d[2]})
.attr('y2', function(d) {return d[3]})
.transition()
.duration(3000)
.ease('linear')
.attr('x1', function(d) {return d[0] + 60})
.attr('y1', function(d) {return d[1]})
.attr('x2', function(d) {return d[2] + 60})
.attr('y2', function(d) {return d[3]})
.each('end', function(d) {
d3.select(this).remove()
lines()
})
})()
})()
body {
padding: 1rem;
}
svg {
background-color: silver;
stroke: black;
stroke-width: 1;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
<div id="sketches"></div>
The issue is the each function will initiate for each line you have. So actually what you are doing is calling lines() three times every time. Why it's yieling the output of one line I'm not entirely sure (still looking into it) but for some reason, it seems like data defaults to the last array so its only setting the drawing to be based on data[3].
To fix it, you want to make sure lines() only gets called after it has finished going through removing all the lines so it only runs once. I'm pretty sure there is better way (i.e. a promise of some kind so after all of each has ran, it'll run a function, but what you can do is set a count and then just run lines() every N times where N is the number of lines you want removed. Because you go through array data and append a line for each index, N is data.length.
(I'm gonna see if there's a cleaner way to do this and I'll edit my answer if I find a way but hopefully this helps you understand the issue at the very least)
(function() {
var w = 100, h = 100
var div = d3.select('#sketches').append('div')
var svg = div.append("svg")
.attr("width", w)
.attr("height", h)
var x = 0, y = 55
var data = [
[x, y, x+20, y-40],
[x+10, y, x+30, y-40],
[x+20, y, x+40, y-40]
];
var count = 0;
(function lines() {
svg.selectAll('line')
.data(data).enter().append('line')
.attr("stroke", "black")
.attr('x1', function(d) {return d[0]})
.attr('y1', function(d) {return d[1]})
.attr('x2', function(d) {return d[2]})
.attr('y2', function(d) {return d[3]})
.transition()
.duration(3000)
.ease('linear')
.attr('x1', function(d) {return d[0] + 60})
.attr('y1', function(d) {return d[1]})
.attr('x2', function(d) {return d[2] + 60})
.attr('y2', function(d) {return d[3]})
.each('end', function(d) {
d3.select(this).remove()
count++;
if (count == data.length) {
count = 0;
lines();
}
})
})()
})()
body {
padding: 1rem;
}
svg {
background-color: silver;
stroke: black;
stroke-width: 1;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
<div id="sketches"></div>

D3 Plot array of circles

I've tried the circle plot example as the following:
var x=20, y=20, r=50;
var sampleSVG = d3.select("#viz")
.append("svg")
.attr("width", 800)
.attr("height", 600);
sampleSVG.append("circle")
.style("stroke", "gray")
.style("fill", "white")
.attr("r", r)
.attr("cx", x)
.attr("cy", y);
But I want to figure out how to plot without a loop a sequence of circles from an array like:
data = [
[10,20,30],
[20,30,15],
[30,10,25]
];
Maybe this example could help?
var data = [
[10,20,30],
[20,30,15],
[30,10,25]
];
var height = 300,
width = 500;
var svg = d3.select('body').append('svg')
.attr('height', height)
.attr('width', width)
.append('g')
.attr('transform', 'translate(30, 30)');
// Bind each nested array to a group element.
// This will create 3 group elements, each of which will hold 3 circles.
var circleRow = svg.selectAll('.row')
.data(data)
.enter().append('g')
.attr('transform', function(d, i) {
return 'translate(30,' + i * 60 + ')';
});
// For each group element 3 circle elements will be appended.
// This is done by binding each element in a nested array to a
// circle element.
circleRow.selectAll('.circle')
.data(function(d, i) { return data[i]; })
.enter().append('circle')
.attr('r', function(d) { return d; })
.attr('cx', function(d, i) { return i * 60; })
.attr('cy', 0);
Live Fiddle

How to make plotting possible on x-axis lines only? (d3.js)

I've just started with trying out the d3 library.
I am trying to create an interactive line chart where people can plot their own points. You can find it over here: http://jsfiddle.net/6FjJ2/
My question is: how can I make sure that plotting can only be done on the x-axis' lines? If you check out my example, you will see it kind of works, but with a lot of cheating. Check out the ok variable... What would be the correct way of achieving this? I have no idea how I can achieve this with a ... so I'm getting a lot of seperate 's.
var data = [2, 3, 4, 3, 4, 5],
w = 1000,
h = 300,
monthsData = [],
months = 18;
for(i = 0; i < months; i++) {
monthsData.push(i);
}
var max = d3.max(monthsData),
x = d3.scale.linear().domain([0, monthsData.length]).range([0, w]),
y = d3.scale.linear().domain([0, max]).range([h, 0]),
pointpos = [];
lvl = [0, 10],
lvly = d3.scale.linear().domain([0, d3.max(lvl)]).range([h, 0]);
svg = d3.select(".chart")
.attr("width", w)
.attr("height", h);
svg.selectAll('path.line')
// Return "data" array which will form the path coordinates
.data([data])
// Add path
.enter().append("svg:path")
.attr("d", d3.svg.line()
.x(function(d, i) { return x(i); })
.y(y));
// Y-axis ticks
ticks = svg.selectAll(".ticky")
// Change number of ticks for more gridlines!
.data(lvly.ticks(10))
.enter().append("svg:g")
.attr("transform", function(d) { return "translate(0, " + (lvly(d)) + ")"; })
.attr("class", "ticky");
ticks.append("svg:line")
.attr("y1", 0)
.attr("y2", 0)
.attr("x1", 0)
.attr("x2", w);
ticks.append("svg:text")
.text( function(d) { return d; })
.attr("text-anchor","end")
.attr("dy", 2)
.attr("dx", -4);
// X-axis ticks
ticks = svg.selectAll(".tickx")
.data(x.ticks(monthsData.length))
.enter().append("svg:g")
.attr("transform", function(d, i) { return "translate(" + (x(i)) + ", 0)"; })
.attr("class", "tickx");
ticks.append("svg:line")
.attr("y1", h)
.attr("y2", 0)
.attr("x1", 0)
.attr("x2", 0);
ticks.append("svg:text")
.text( function(d, i) { return i; })
.attr("y", h)
.attr("dy", 15)
.attr("dx", -2);
// var d = $(".tickx:first line").css({"stroke-width" : "2", opacity : "1"});
var line;
var ok = -55;
svg.on("mousedown", mouseDown)
.on("mouseup", mouseUp);
function mouseDown() {
var m = d3.mouse(this);
line = svg.append("line")
.data(monthsData)
/* .attr("x1", m[0]) */
.attr("x1", function(d, i) { pointpos.push(m[0]); ok += 55; return ok;})
.attr("y1", m[1])
.attr("x2", function(d, i) { return ok + 56; })
/* .attr("x2", function(d, i) {return 300; }) */
.attr("y2", m[1]);
svg.on("mousemove", mouseMove);
var m = d3.mouse(this);
var point = svg.append("circle")
.attr("cx", function(d, i) { return ok; })
.attr("cy", function(d, i) { return m[1]; })
.attr("r", 8);
lvl.push(100);
}
function mouseMove() {
var m = d3.mouse(this);
line.attr("y2", m[1]);
/* .attr("y1", m[0]); */
}
function mouseUp() {
// Change null to mousemove for a graph kinda draw mode
svg.on("mousemove", mouseMove);
}
Excuse my bad code!
Thanks in advance.
It looks like you need:
histogram layout for binning your points.
ordinal scales for restricting their x-axis positions according to the bin
As a sidenote, you can use d3.svg.axis to draw the axis for you.

Nested SVG selections in D3

I have a list of n associative arrays.
[{'path': 'somepath', 'relevant': [7, 8, 9]}, {'path': 'anotherpath', 'relevant': [9], ...}
Within a large SVG, I want to: a) create rects ("buckets") whose dimensions are proportional to the lengths of their 'relevant' sublists, and b) create rects ("pieces"), for each of the elements in the sublists, placed "inside" their respective buckets.
After reading Mike Bostock's response to a similar question, I'm sure that I need to use group ("g") elements to group the pieces together. I can get the code below to produce the DOM tree that I want, but I'm stumped on how to code the y values of the pieces. At the point where I need the value, D3 is iterating over the subarrays. How can I get the index of the current subarray that I'm iterating over from inside it when i no longer points to the index within the larger array?
var piece_bukcets = svg.selectAll("g.piece_bucket")
.data(files)
.enter()
.append("g")
.attr("class", "piece_bucket")
.attr("id", function (d, i) { return ("piece_bucket" + i) })
.append("rect")
.attr("y", function (d, i) { return (i * 60) + 60; })
.attr("class", "bar")
.attr("x", 50)
.attr("width", function (d) {
return 10 * d["relevant"].length;
})
.attr("height", 20)
.attr("fill", "red")
.attr("opacity", 0.2)
var pieces = svg.selectAll("g.piece_bucket")
.selectAll("rect.piece")
.data( function (d) { return d["relevant"]; })
.enter()
.append("rect")
.attr("class", "piece")
.attr("id", function (d) { return ("piece" + d) })
.attr("y", ????) // <<-- How do I get the y value of d's parent?
.attr("x", function (d, i) { return i * 10; })
.attr("height", 10)
.attr("width", 10)
.attr("fill", "black");
Is there a method on d available to find the index of the node it's currently inside? In this case, is there a method I can call on a "piece" to find the index of its parent "bucket"?
You can use the secret third argument to the function:
.attr("y", function(d, i, j) {
// j is the i of the parent
return (j * 60) + 60;
})
There's a simpler way however. You can simply translate the g element and everything you add to it will fall into place.
var piece_buckets = svg.selectAll("g.piece_bucket")
.data(files)
.enter()
.append("g")
.attr("class", "piece_bucket")
.attr("transform", function(d, i) {
return "translate(0," + ((i*60) + 60) + ")";
})
.attr("id", function (d, i) { return ("piece_bucket" + i) });
piece_buckets.append("rect")
.attr("class", "bar")
.attr("x", 50)
.attr("width", function (d) {
return 10 * d["relevant"].length;
})
.attr("height", 20)
.attr("fill", "red")
.attr("opacity", 0.2);
var pieces = piece_buckets.selectAll("rect.piece")
.data(function (d) { return d["relevant"]; })
.enter()
.append("rect")
.attr("class", "piece")
.attr("id", function (d) { return ("piece" + d); })
.attr("x", function (d, i) { return i * 10; })
.attr("height", 10)
.attr("width", 10)
.attr("fill", "black");

Categories

Resources