I'm having trouble trying to create a transition of groups. I've created an object from a path based on x and y data. I want this object to transition horizontally 5 times. I have 5 groups that will use the same object and transition. I'm trying to use a for loop so I don't have to rewrite that transition 5 times. You'll see in the snippet I have the groups group1, group2, etc., that are meant to use the same object. Is it possible to create an array of those groups that can be used for the transition? Hopefully this isn't confusing, and I'm using d3.v4. Thank you for taking a look at this.
var width = 900
var height = 1200;
var canvas = d3.select('body')
.append('svg')
.attr('width', width)
.attr('height', height);
// x and y coordinates of the path
var objectArray = [{x: 51, y: 44},
{x: 51, y: 49},
{x: 53, y: 50},
{x: 53, y: 53},
{x: 52, y: 53},
{x: 52, y: 60},
{x: 70, y: 85},
{x: 71, y: 160},
{x: 64, y: 181},
{x: 54, y: 181},
{x: 47, y: 170},
{x: 43, y: 170},
{x: 36, y: 181},
{x: 26, y: 181},
{x: 19, y: 160},
{x: 19, y: 85},
{x: 39, y: 60},
{x: 39, y: 53},
{x: 38, y: 53},
{x: 38, y: 50},
{x: 40, y: 49},
{x: 40, y: 44},
{x: 51, y: 44}];
var interpolate = d3.curveCardinal.tension(0.35);
var objectOutline = d3.line()
.x(function(d, i) { return d.x / 2.6 })
.y(function(d, i) { return d.y / 2.2 })
.curve(interpolate);
// The 5 groups that use that same shapeArray for transition
var group1 = canvas.append('g');
var group2 = canvas.append('g');
var group3 = canvas.append('g');
var group4 = canvas.append('g');
var group5 = canvas.append('g');
// The group coordinates for start and end of transition
var locationArray = [{x:1200, y:440, xTransition:250},
{x:1200, y:440, xTransition:278},
{x:1200, y:440, xTransition:306},
{x:1200, y:440, xTransition:334},
{x:1200, y:440, xTransition:362}];
var length = locationArray.length;
for (var i=0; i < length; i++){
// Right now the group1 group is being used for the path
group1.selectAll('path')
.data([objectArray])
.enter()
.append('path')
.attr('d', objectOutline)
group1.attr('transform','translate('+ locationArray[i].x + ','+ locationArray[i].y + ')')
.transition()
.duration((Math.random() * 3000) + 800)
.delay(Math.random() * 3000)
.attr('transform', 'translate(' + locationArray[i].xTransition + ',' + locationArray[i].y + ')');
}
The problem with a for loop is that it will run to the end in few milliseconds, that is, almost immediately. That's not how you create a series of transitions.
The idiomatic way for doing this is using .on("end"... at the end of each transition. For instance, in this snippet, I create a function named transitioning that is called when the transition ends. As you have just 5 elements in the array, I set a counter, and the transition is called only when the counter is < 5:
transitioning(1)
function transitioning(index) {
console.log(locationArray[index].y)
path.transition()
.duration((Math.random() * 3000) + 800)
.delay(Math.random() * 3000)
.attr('transform', 'translate(' + locationArray[index].xTransition + ',' + locationArray[index].y + ')')
.on("end", function() {
if (++index > 4) return;
transitioning(index)
})
}
I'm starting at 1 because the bottle is already at position 0. Also, it's funny that you're transitioning the group, not the path. I changed that. If that's not what you want, just use the same logic to the groups.
Here is your code with this new function:
var width = 600
var height = 500;
var canvas = d3.select('body')
.append('svg')
.attr('width', width)
.attr('height', height);
// x and y coordinates of the path
var objectArray = [{
x: 51,
y: 44
}, {
x: 51,
y: 49
}, {
x: 53,
y: 50
}, {
x: 53,
y: 53
}, {
x: 52,
y: 53
}, {
x: 52,
y: 60
}, {
x: 70,
y: 85
}, {
x: 71,
y: 160
}, {
x: 64,
y: 181
}, {
x: 54,
y: 181
}, {
x: 47,
y: 170
}, {
x: 43,
y: 170
}, {
x: 36,
y: 181
}, {
x: 26,
y: 181
}, {
x: 19,
y: 160
}, {
x: 19,
y: 85
}, {
x: 39,
y: 60
}, {
x: 39,
y: 53
}, {
x: 38,
y: 53
}, {
x: 38,
y: 50
}, {
x: 40,
y: 49
}, {
x: 40,
y: 44
}, {
x: 51,
y: 44
}];
var interpolate = d3.curveCardinal.tension(0.35);
var objectOutline = d3.line()
.x(function(d, i) {
return d.x / 2.6
})
.y(function(d, i) {
return d.y / 2.2
})
.curve(interpolate);
// The 5 groups that use that same shapeArray for transition
var group1 = canvas.append('g');
// The group coordinates for start and end of transition
var locationArray = [{
x: 200,
y: 40,
xTransition: 20
}, {
x: 200,
y: 40,
xTransition: 80
}, {
x: 200,
y: 40,
xTransition: 140
}, {
x: 200,
y: 40,
xTransition: 220
}, {
x: 200,
y: 40,
xTransition: 280
}];
var length = locationArray.length;
// Right now the group1 group is being used for the path
var path = group1.selectAll('path')
.data([objectArray])
.enter()
.append('path')
.attr('d', objectOutline)
.attr('transform', 'translate(' + locationArray[0].xTransition + ',' + locationArray[0].y + ')')
transitioning(1)
function transitioning(index) {
path.transition()
.duration((Math.random() * 3000) + 800)
.delay(Math.random() * 1000)
.attr('transform', 'translate(' + locationArray[index].xTransition + ',' + locationArray[index].y + ')')
.on("end", function() {
if (++index > 4) return;
transitioning(index)
})
}
<script src="https://d3js.org/d3.v4.min.js"></script>
Related
I need to access an object and it's property and check if the the value of the property smaller than 100 is.
My code would look like following:
let myArr = [{
id: 1,
x: 120,
y: 150,
}, {
id: 2,
x: 170,
y: 420,
}, {
id: 3,
x: 160,
y: 220,
}, {
id: 4,
x: 140,
y: 170,
}];
if(nearestEnemy.x - /*go throught all of my "x"-properties*/ && nearestEnemy.y - /*go throught all of my "y"-properties*/ < 100){
}
You don't need to matter about the other code, just look at my comments.
I want to check if the x axis and the y axis is almost the same as of one of my properties from my object in my array.
I guess you'd need something like a loop for this but I can't think of anything!
I don't know if you can understand me because I cant really explain what I mean.
Thanks for you help anyway.
You can achieve this by filtering out the objects by using Array.filter() method.
let myArr = [{
id: 1,
x: 120,
y: 150
}, {
id: 2,
x: 170,
y: 420
}, {
id: 3,
x: 160,
y: 220
}, {
id: 4,
x: 140,
y: 170
}];
function difference(a, b) {
return Math.abs(a - b);
}
const filtered = myArr.filter(({x, y}) => difference(x, y) < 100);
console.log(filtered);
It sounds to me like you have an array of points and want to find an element which is the nearest to the given point. If this is the case, you can proceed like this:
write a function that computes distance (https://en.wikipedia.org/wiki/Euclidean_distance) between two points
write a loop that minimizes distance
Example:
let myArr = [
{
id: 1,
x: 120,
y: 150,
}, {
id: 2,
x: 170,
y: 420,
}, {
id: 3,
x: 160,
y: 220,
}, {
id: 4,
x: 140,
y: 170,
}];
function distance(p1, p2) {
return Math.sqrt(
(p1.x - p2.x) ** 2 + (p1.y - p2.y) ** 2,
)
}
function nearestPoint(points, somePoint) {
let min = +Infinity,
minPt = null;
for (let p of points) {
let d = distance(p, somePoint);
if (d < min) {
min = d;
minPt = p;
}
}
return minPt;
}
console.log(nearestPoint(myArr, {x: 190, y: 130}))
This is not very efficient, but should be fine for < 10,000 points. If you have more, you'll need some kind of spatial indexing.
I have coordinates (x, y) of place where I am currently and array of objects with coordinates of destinations.
Coordinates x and y are just representing of fields in map like this:
and it is up to 100x100.
myPosition is array: [ 13, 11 ] where 13 is x and 11 is y (demonstratively marked it as red in above jpg).
destinations is array of object:
[ { x: 22, y: 13 },
{ x: 16, y: 25 },
{ x: 20, y: 11 },
{ x: 76, y: 49 },
{ x: 65, y: 47 },
{ x: 82, y: 33 },
{ x: 86, y: 35 },
{ x: 61, y: 59 },
{ x: 62, y: 52 },
{ x: 18, y: 52 },
{ x: 24, y: 49 },
{ x: 52, y: 55 },
{ x: 20, y: 57 },
{ x: 80, y: 11 },
{ x: 55, y: 61 },
{ x: 46, y: 59 },
{ x: 77, y: 19 },
{ x: 2, y: 22 },
{ x: 78, y: 23 },
{ x: 86, y: 51 },
{ x: 75, y: 46 },
{ x: 6, y: 8 },
{ x: 25, y: 12 },
{ x: 81, y: 21 },
{ x: 53, y: 58 } ]
I need some tips or algorithm which will sort destination array of object in order from closest to my position.
What you got is called the nearest neighbor problem.
Two solutions, depening on where you need to take your problem:
You could simply do a brute force distance search with the usual optimization of skipping the square-root:
Some code:
function DistSquared(pt1, pt2) {
var diffX = pt1.x - pt2.x;
var diffY = pt1.y - pt2.y;
return (diffX*diffX+diffY*diffY);
}
closest = destinations[0];
shortestDistance = DistSquared(myPosition, destinations[0]);
for (i = 0; i < destinations.length; i++) {
var d = DistSquared(myPosition, destinations[i]);
if (d < shortestDistance) {
closest = destinations[i];
shortestDistance = d;
}
}
If you are going to make repeated nearest neighbor queries on the same destinations array, you can use an algorithm known as a k-d tree. This where you build a binary tree representation of your grid of points by partitioning it in half many times along a different axis.
I'm trying to display a number of circles wrapped in <g> elements. This wrapping thing is important for me as in real world I want other elements to be in my group as well. The problem is, circles in the below example wont get displayed by first render(myObjects) call, and if you hit render button they are shown.
Second problem is when I press update the previously drawn circles are not removed and the new ones will render on top of them.
var svg = d3.select('body').append('svg')
.attr('width', 250)
.attr('height', 250);
//render the data
function render(datum) {
//Bind
var groups = svg.selectAll("g").data(datum);
groups.enter().append('g');
var circles = groups.append("circle").attr('r', 10)
.attr('cx', function(d) {
return d.x;
})
.attr('cy', function(d) {
return d.y;
});
groups.exit().remove();
}
var myObjects = [{ x: 100, y: 100 }, { x: 130, y: 120 }, { x: 80, y: 180 }, { x: 180, y: 80 }, { x: 180, y: 40 }];
var newObjects = [{ x: 200, y: 120 }, { x: 150, y: 160 }, { x: 20, y: 80 }, { x: 80, y: 60 }, { x: 100, y: 20 }];
function removeEls() {
svg.selectAll('circle').remove();
}
render(myObjects);
svg {
width: 500px;
height: 500px
}
button {
float: left
}
text {
text-anchor: middle;
}
circle {
fill: orange;
stroke: orange;
fill-opacity: 0.5;
}
<script src="https://d3js.org/d3.v4.min.js"></script>
<button onclick="render(newObjects)">Update</button>
<button onclick="render(myObjects);">Render</button>
<button onclick="removeEls();">removeEls</button>
You have 2 little issues in your code :
You have added the circles in the groups instead of groups.enter(), hence the issue of not displaying the circles at the first draw
You forget to add an identifier when you add the data, so the script is unable to detect which data is new and which data is not. I added a d.x/d.y identifier, which works in your case, but I advice you to add a true unique identifier. Like add a field id in your list of objects.
Here is the snippet with the corrections :
var svg = d3.select('body').append('svg')
.attr('width', 250)
.attr('height', 250)
.append('g');
//render the data
function render(datum) {
//Bind
var groups = svg.selectAll("g").data(datum, d => (d.x)/(d.y));
var groupEnter = groups.enter().append('g');
var circles = groupEnter.append("circle").attr('r', 10)
.attr('cx', function(d) {
return d.x;
})
.attr('cy', function(d) {
return d.y;
});
groups.exit().remove();
}
var myObjects = [{ x: 100, y: 100 }, { x: 130, y: 120 }, { x: 80, y: 180},{ x: 180, y: 80 }, { x: 180, y: 40 }];
var newObjects = [{ x: 200, y: 120 }, { x: 150, y: 160 }, { x: 20, y: 80}, { x: 80, y: 60 }, { x: 100, y: 20 }];
function removeEls() {
svg.selectAll('circle').remove();
}
render(myObjects);
svg {
width: 500px;
height: 500px
}
button {
float: left
}
text {
text-anchor: middle;
}
circle {
fill: orange;
stroke: orange;
fill-opacity: 0.5;
}
<script src="https://d3js.org/d3.v4.min.js"></script>
<button onclick="render(newObjects)">Update</button>
<button onclick="render(myObjects);">Render</button>
<button onclick="removeEls();">removeEls</button>
I've made a line path for creating a shape of a bottle. I'm trying to figure out if I can close this path in order to transition it. Ideally I would like to have the bottle animate across the canvas, the same way a circle or rectangle would. Is this possible? I'm still learning d3.
var width = 900,
height = 800;
var canvas = d3.select('body')
.append('svg')
.attr('width', width)
.attr('height', height);
var dataArray = [
{x:51,y:44},
{x:51,y:49},
{x:53,y:50},
{x:53,y:53},
{x:52,y:53},
{x:52,y:60},
{x:70,y:85},
{x:71,y:160},
{x:64,y:181},
{x:54,y:181},
{x:47,y:170},
{x:43,y:170},
{x:36,y:181},
{x:26,y:181},
{x:19,y:160},
{x:19,y:85},
{x:39,y:60},
{x:39,y:53},
{x:38,y:53},
{x:38,y:50},
{x:40,y:49},
{x:40,y:44},
{x:51,y:44}
];
var interpolate = d3.curveCardinal.tension(0.35);
var line = d3.line()
.x(function(d,i){ return d.x/1.05 })
.y(function(d,i){ return d.y })
.curve(interpolate);
var group = canvas.append('g')
.attr('transform','translate(0,0)');
var bottle = group.selectAll('path')
.data([dataArray])
.enter()
.append('path')
.attr('fill','#ED5545')
.attr('stroke','#AA2731')
.attr('stroke-width','2')
.attr('id','bottleImage')
.attr('d',line);
bottle.transition()
.attr('x',300);
A SVG path has no x attribute. The simplest solution, therefore, is applying the transition to the group:
group.transition()
.delay(500)
.duration(2000)
.attr('transform', 'translate(300,0)');
Here is the demo:
var width = 400,
height = 300;
var canvas = d3.select('body')
.append('svg')
.attr('width', width)
.attr('height', height);
var dataArray = [{
x: 51,
y: 44
}, {
x: 51,
y: 49
}, {
x: 53,
y: 50
}, {
x: 53,
y: 53
}, {
x: 52,
y: 53
}, {
x: 52,
y: 60
}, {
x: 70,
y: 85
}, {
x: 71,
y: 160
}, {
x: 64,
y: 181
}, {
x: 54,
y: 181
}, {
x: 47,
y: 170
}, {
x: 43,
y: 170
}, {
x: 36,
y: 181
}, {
x: 26,
y: 181
}, {
x: 19,
y: 160
}, {
x: 19,
y: 85
}, {
x: 39,
y: 60
}, {
x: 39,
y: 53
}, {
x: 38,
y: 53
}, {
x: 38,
y: 50
}, {
x: 40,
y: 49
}, {
x: 40,
y: 44
}, {
x: 51,
y: 44
}];
var interpolate = d3.curveCardinal.tension(0.35);
var line = d3.line()
.x(function(d, i) {
return d.x / 1.05
})
.y(function(d, i) {
return d.y
})
.curve(interpolate);
var group = canvas.append('g')
.attr('transform', 'translate(0,0)');
var bottle = group.selectAll('path')
.data([dataArray])
.enter()
.append('path')
.attr('fill', '#ED5545')
.attr('stroke', '#AA2731')
.attr('stroke-width', '2')
.attr('id', 'bottleImage')
.attr('d', line);
group.transition()
.delay(500)
.duration(2000)
.attr('transform', 'translate(300,0)');
<script src="https://d3js.org/d3.v4.min.js"></script>
I'm trying to do responsive chart... i used this but its not responsive just when i refresh the page it scaled to right width... Any other ideas then i had ?
And i try to set a div with viewBox and preserveAspectRatio.. But the same result..
var graph = new Rickshaw.Graph( {
element: document.querySelector("#chart"),
height: 100,
series: [{
color: '#1abc9c',
data: [
{ x: 0, y: 50 },
{ x: 1, y: 50 },
{ x: 2, y: 40 },
{ x: 3, y: 40 },
{ x: 4, y: 30 },
{ x: 5, y: 20 },
{ x: 6, y: 0 },
{ x: 7, y: 0 } ]
}]
});
graph.render();
var svg = d3.select('.chart-container').append("svg")
.attr("width", '100%')
.attr("height", '100%')
.attr('viewBox','0 0 '+Math.min(width,height)+' '+Math.min(width,height))
.attr('preserveAspectRatio','xMinYMin')
.append("g")
.attr("transform", "translate(" + Math.min(width,height) / 2 + "," + Math.min(width,height) / 2 + ")");
<div class="chart-container" id="chart"></div>
Ok thanks to Mark Sizer.. Solution is here :
var seriesData = [ [
{ x: 0, y: 50 },
{ x: 1, y: 50 },
{ x: 2, y: 50 },
{ x: 3, y: 40 },
{ x: 4, y: 40 },
{ x: 5, y: 20 },
{ x: 6, y: 0 },
{ x: 7, y: 0 } ] ];
var graph = new Rickshaw.Graph( {
element: document.getElementById("chart"),
renderer: 'area',
series: [
{
data: seriesData[0],
color: '#1abc9c'
}
]
} );
var resize = function() {
graph.configure({
width: window.innerWidth * 0,
height: window.innerHeight * 0.20
});
graph.render();
}
window.addEventListener('resize', resize);
resize();
<script src="//cdnjs.cloudflare.com/ajax/libs/d3/3.5.5/d3.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/rickshaw/1.5.1/rickshaw.js"></script>
<div id="chart"></div>