Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why do some cells not move entirely

I have set up this jsfiddle : http://jsfiddle.net/386er/dhzq6q6f/14/

var moveCell = function(direction) {

var cellToBeMoved = pickRandomCell();
var currentX = cellToBeMoved.x.baseVal.value; 
var currentY = cellToBeMoved.y.baseVal.value;
var change = getPlusOrMinus() * (cellSize + 1 );
var newX = currentX + change;
var newY = currentY + change;
var selectedCell = d3.select(cellToBeMoved);


if (direction === 'x') {
    selectedCell.transition().duration(1500)
        .attr('x', newX );
} else {
    selectedCell.transition().duration(1500)
        .attr('y', newY );

}

}

In the moveCell function, I pick a random cell, request its current x and y coordinates and then add or subtract its width or height, to move it to an adjacent cell.

What I am wondering about: If you watch the cells move, some will only move partially to the next cell. Can anoyne tell me, why this is so ?

like image 518
386er Avatar asked May 19 '15 10:05

386er


1 Answers

The first thing to do in this situation is put .each("interrupt", function() { console.log("interrupted!") }); on your transitions. Then you will see the problem.
Its supposed to fix it if you name the transitions like selection.transition("name"), but that doesn't fix it.
That means you have to do as suggested by @jcuenod and exclude the ones that are moving. One way to do that which is idiomatic is like this...

if (direction === 'x') {
    selectedCell.transition("x").duration(1500)
      .attr('x', newX)
      .each("start", function () { lock.call(this, "lockedX") })
      .each("end", function () { unlock.call(this, "lockedX") });
} else {
    selectedCell.transition("y").duration(1500)
      .attr('y', newY)
      .each("start", function () { lock.call(this, "lockedX") })
      .each("end", function () { unlock.call(this, "lockedX") });
}

function lock(lockClass) {
    var c = { cell: false }; c[lockClass] = true;
    d3.select(this).classed(c)
};
function unlock(lockClass) {
    var c = { cell: this.classList.length == 1 }; c[lockClass] = false;
    d3.select(this).classed(c);
};

Here is a fiddle to prove the concept.


Pure and idiomatic d3 version

Just for completeness here is the d3 way to do it.
I've tried to make it as idiomatic as possible. The main points being...

  1. Purely data-driven
    The data is updated and the viz manipulation left entirely to d3 declarations.
  2. Use d3 to detect and act on changes to svg element attributes
    This is done by using a composite key function in the selection.data() method and by exploiting the fact that changed nodes (squares where the x, y or fillattributes don't match the updated data) are captured by the exit selection.
  3. Splice changed elements into the data array so d3 can detect changes
    Since a reference to the data array elements is bound to the DOM elements, any change to the data will also be reflected in the selection.datum(). d3 uses a key function to compare the data values to the datum, in order to classify nodes as update, enter or exit. If a key is made, that is a function of the data/datum values, changes to data will not be detected. By splice-ing changes into the data array, the value referenced by selection.datum() will be different from the data array, so data changes will flag exit nodes.
    By simply manipulating attributes and putting transitions on the exit selection and not removing it, it essentially becomes a 'changed' selection.
    this only works if the data values are objects.
  4. Concurrent transitions
    Named transitions are used to ensure x and y transitions don't interrupt each other, but it was also necessary to use tag class attributes to lock out transitioning elements. This is done using transition start and end events.
  5. Animation frames
    d3.timer is used to smooth animation and marshal resources. d3Timer calls back to update the data before the transitions are updated, before each animation frame.
  6. Use d3.scale.ordinal() to manage positioning
    This is great because you it works every time and you don't even have to thin about it.

$(function () {
	var container,
		svg,
		gridHeight = 800,
		gridWidth = 1600,
		cellSize, cellPitch,
		cellsColumns = 100,
		cellsRows = 50,
		squares,

		container = d3.select('.svg-container'),
		svg = container.append('svg')
			.attr('width', gridWidth)
			.attr('height', gridHeight)
			.style({ 'background-color': 'black', opacity: 1 }),

		createRandomRGB = function () {
			var red = Math.floor((Math.random() * 256)).toString(),
					green = Math.floor((Math.random() * 256)).toString(),
					blue = Math.floor((Math.random() * 256)).toString(),
					rgb = 'rgb(' + red + ',' + green + ',' + blue + ')';
			return rgb;
		},

		createGrid = function (width, height) {

			var scaleHorizontal = d3.scale.ordinal()
						.domain(d3.range(cellsColumns))
						.rangeBands([0, width], 1 / 15),
					rangeHorizontal = scaleHorizontal.range(),

					scaleVertical = d3.scale.ordinal()
						.domain(d3.range(cellsRows))
						.rangeBands([0, height]),
					rangeVertical = scaleVertical.range(),

					squares = [];
			rangeHorizontal.forEach(function (dh, i) {
				rangeVertical.forEach(function (dv, j) {
					var indx;
					squares[indx = i + j * cellsColumns] = { x: dh, y: dv, c: createRandomRGB(), indx: indx }
				})
			});

			cellSize = scaleHorizontal.rangeBand();
			cellPitch = {
				x: rangeHorizontal[1] - rangeHorizontal[0],
				y: rangeVertical[1] - rangeVertical[0]
			}

			svg.selectAll("rect").data(squares, function (d, i) { return d.indx })
				.enter().append('rect')
				.attr('class', 'cell')
				.attr('width', cellSize)
				.attr('height', cellSize)
				.attr('x', function (d) { return d.x })
				.attr('y', function (d) { return d.y })
				.style('fill', function (d) { return d.c });

			return squares;
		},

		choseRandom = function (options) {
			options = options || [true, false];
			var max = options.length;
			return options[Math.floor(Math.random() * (max))];
		},

		pickRandomCell = function (cells) {

				var l = cells.size(),
						r = Math.floor(Math.random() * l);

				return l ? d3.select(cells[0][r]).datum().indx : -1;

		};

	function lock(lockClass) {
		var c = { cell: false }; c[lockClass] = true;
		d3.select(this).classed(c)
	};
	function unlock(lockClass) {
		var c = { cell: this.classList.length == 1 }; c[lockClass] = false;
		d3.select(this).classed(c);
	};

	function permutateColours() {
		var samples = Math.min(50, Math.max(~~(squares.length / 50),1)), s, ii = [], i, k = 0,
				cells = d3.selectAll('.cell');
		while (samples--) {
			do i = pickRandomCell(cells); while (ii.indexOf(i) > -1 && k++ < 5 && i > -1);
			if (k < 10 && i > -1) {
				ii.push(i);
				s = squares[i];
				squares.splice(i, 1, { x: s.x, y: s.y, c: createRandomRGB(), indx: s.indx });
			}
		}

	}

	function permutatePositions() {
		var samples = Math.min(20, Math.max(~~(squares.length / 100),1)), s, ss = [], d, m, p, k = 0,
				cells = d3.selectAll('.cell');
		while (samples--) {
			do s = pickRandomCell(cells); while (ss.indexOf(s) > -1 && k++ < 5 && s > -1);
			if (k < 10 && s > -1) {
				ss.push(s);
				d = squares[s];
				m = { x: d.x, y: d.y, c: d.c, indx: d.indx };
				m[p = choseRandom(["x", "y"])] = m[p] + choseRandom([-1, 1]) * cellPitch[p];

				squares.splice(s, 1, m);
			}
		}
	}

	function updateSquares() {
        //use a composite key function to transform the exit selection into
        // an attribute update selection
        //because it's the exit selection, d3 doesn't bind the new data
        // that's done manually with the .each 
		var changes = svg.selectAll("rect")
			.data(squares, function (d, i) { return d.indx + "_" + d.x + "_" + d.y + "_" + d.c; })
			.exit().each(function (d, i, j) { d3.select(this).datum(squares[i]) })

		changes.transition("x").duration(1500)
			.attr('x', function (d) { return d.x })
			.each("start", function () { lock.call(this, "lockedX") })
			.each("end", function () { unlock.call(this, "lockedX") })

		changes.transition("y").duration(1500)
			.attr('y', function (d) { return d.y })
			.each("start", function () { lock.call(this, "lockedY") })
			.each("end", function () { unlock.call(this, "lockedY") });

		changes.attr("stroke", "white")
			.style("stroke-opacity", 0.6)
			.transition("fill").duration(800)
				.style('fill', function (d, i) { return d.c })
				.style("stroke-opacity", 0)
				.each("start", function () { lock.call(this, "lockedFill") })
				.each("end", function () { unlock.call(this, "lockedFill") });
	}
	squares = createGrid(gridWidth, gridHeight);

	d3.timer(function tick() {
		permutateColours();
		permutatePositions();
		updateSquares();
	});
});
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.5/d3.min.js"></script>
  <div class="svg-container"></div>

NOTE: requires d3 version 3.5.5 for the position transitions to run.

EDIT: fixed a problem with lock and un-lock. Would probably better to tag the data rather than write classes to the DOM but, anyway... this way is fun.

like image 74
Cool Blue Avatar answered Oct 29 '22 21:10

Cool Blue