I have a nvd3 chart example, however i have no idea how to enter the data into the graph and make use of it.
HTML
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<link href="build/nv.d3.css" rel="stylesheet" type="text/css">
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.2/d3.min.js" charset="utf-8"></script>
<script src="build/nv.d3.js"></script>
<script src="lib/stream_layers.js"></script>
<style>
text {
font: 12px sans-serif;
}
svg {
display: block;
}
html, body, #test1, svg {
margin: 0px;
padding: 0px;
height: 100%;
width: 100%;
}
</style>
</head>
<body>
<div id="test1">
<svg></svg>
</div>
<script>
//var test_data = stream_layers(3,128,.1).map(function(data, i) {
var test_data = stream_layers(3, 128, .1).map(function (data, i) {
return {
key: (i == 1) ? 'Non-stackable Stream' + i : 'Stream' + i,
nonStackable: (i == 1),
values: data
};
});
nv.addGraph({
generate: function () {
var width = nv.utils.windowSize().width,
height = nv.utils.windowSize().height;
var chart = nv.models.multiBarChart()
.width(width)
.height(height)
.stacked(true)
;
chart.dispatch.on('renderEnd', function () {
console.log('Render Complete');
});
var svg = d3.select('#test1 svg').datum(test_data);
console.log('calling chart');
svg.transition().duration(0).call(chart);
return chart;
},
callback: function (graph) {
nv.utils.windowResize(function () {
var width = nv.utils.windowSize().width;
var height = nv.utils.windowSize().height;
graph.width(width).height(height);
d3.select('#test1 svg')
.attr('width', width)
.attr('height', height)
.transition().duration(0)
.call(graph);
});
}
});
</script>
</body>
</html>
Java(Where the data is taken from)
/* Inspired by Lee Byron's test data generator. */
function stream_layers(n, m, o) {
if (arguments.length < 3) o = 0;
function bump(a) {
var x = 1 / (.1 + Math.random()),
y = 2 * Math.random() - .5,
z = 10 / (.1 + Math.random());
for (var i = 0; i < m; i++) {
var w = (i / m - y) * z;
a[i] += x * Math.exp(-w * w);
}
}
return d3.range(n).map(function() {
var a = [], i;
for (i = 0; i < m; i++) a[i] = o + o * Math.random();
for (i = 0; i < 5; i++) bump(a);
return a.map(stream_index);
});
}
/* Another layer generator using gamma distributions. */
function stream_waves(n, m) {
return d3.range(n).map(function(i) {
return d3.range(m).map(function(j) {
var x = 20 * j / m - i / 3;
return 2 * x * Math.exp(-.5 * x);
}).map(stream_index);
});
}
function stream_index(d, i) {
return {x: i, y: Math.max(0, d)};
}
So as seen above the data for the example is randomly generated.
If someone can give me a reference, or an example of how do i enter data into the grouped bar chart. It will really help me a lot.
What im trying to add in my data into is this
http://nvd3.org/livecode/index.html#codemirrorNav
The grouped bar chart example.
I really am new to this javascript coding, so all help is truly appreciated.
An example from the NVD3 website:
nv.addGraph(function() {
var chart = nv.models.multiBarChart()
.transitionDuration(350)
.reduceXTicks(true) //If 'false', every single x-axis tick label will be rendered.
.rotateLabels(0) //Angle to rotate x-axis labels.
.showControls(true) //Allow user to switch between 'Grouped' and 'Stacked' mode.
.groupSpacing(0.1) //Distance between each group of bars.
;
chart.xAxis
.tickFormat(d3.format(',f'));
chart.yAxis
.tickFormat(d3.format(',.1f'));
d3.select('#chart1 svg')
.datum(test_data)
.call(chart);
nv.utils.windowResize(chart.update);
return chart;
});
So, crucial here is to have element with id="chart1" and inside that element to have an empty svg element (I'm explaining the above setup, it can be different id's and directly only svg element)
The important part is that the data format must be in a specific format. So it should be a JSON object, something like this:
test_data = [
{
values: [{x,y},{x,y}],
key: 'some key',
color: 'some color'
},....
{
values: [{x,y},{x,y}],
key: 'some key',
color: 'some color'
}
];
In your case I see strange the generate and callback functions, which I see that it is a little bit mixed up.
Reference for the above example:
http://nvd3.org/examples/multiBar.html
And refer to the latest documentation and examples:
http://nvd3-community.github.io/nvd3/examples/documentation.html
Related
I have an annotated line chart built successfully from a CSV file using google annotation charts. (Thanks Whitehat for your help).
I have looked unsuccessfully through the google chart examples to find a way of grabbing a slice of the line chart so as to then perform some calculations between the two points e.g. difference and percentage difference. There may be further calculations I wish to do but these two are enough for the moment.
Essentially I am trying to build a feature like Google's stock chart
Code so far:
<html>
<head>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery-csv/0.71/jquery.csv-0.71.min.js"></script>
<script type="text/javascript" src="https://www.gstatic.com/charts/loader.js"></script>
<script type='text/javascript'>
// load google charts
google.charts.load('current', {
packages: ['annotationchart']
}).then(function () {
// declare data variable
var arrayData;
// get csv data
$.get('test.csv', function(csvString) {
// get csv data success, convert to an array, draw chart
arrayData = $.csv.toArrays(csvString, {onParseValue: $.csv.hooks.castToScalar});
drawChart(arrayData);
})
});
// draw chart
function drawChart(arrayData) {
// convert string in first column to a date
arrayData = arrayData.map(function (row) {
return [new Date(row[0]),row[1],row[2]];
});
// create google data table, chart, and options
var data = google.visualization.arrayToDataTable(arrayData);
var chart = new google.visualization.AnnotationChart(document.getElementById('chart_div'));
var options = {
displayAnnotations: true
};
// draw chart
chart.draw(data, options);
}
</script>
</head>
<body>
<div id='chart_div' style='width: 1200x; height: 700px;'></div>
</body>
</html>
Any ideas how I can go about doing this?
you could use mouse events to allow the user to draw a selection on the chart.
given the coordinates of the selection,
use chart methods getChartLayoutInterface & getHAxisValue,
to determine the range of values the user selected.
see following working snippet,
click the chart and hold the mouse, then drag to draw the selection.
when the mouse is let go, the values selected will be displayed.
google.charts.load('current', {
packages: ['controls', 'corechart']
}).then(function () {
// build data table
var oneDay = (24 * 60 * 60 * 1000);
var dateEnd = new Date();
var dateStart = new Date(dateEnd.getTime() - (oneDay * 365.25));
var data = new google.visualization.DataTable();
data.addColumn('date', 'Date');
data.addColumn('number', 'Y');
for (var i = dateStart.getTime(); i <= dateEnd.getTime(); i = i + oneDay) {
var direction = (i % 2 === 0) ? 1 : -1;
var rowDate = new Date(i);
data.addRow([rowDate, rowDate.getFullYear() + (rowDate.getDate() * direction)]);
}
// chart options
var options = {
chartArea: {
height: '100%',
width: '100%',
top: 24,
left: 60,
right: 16,
bottom: 60
},
hAxis: {
format: 'MMM-yyyy'
},
height: '100%',
legend: {
position: 'top'
},
width: '100%'
};
// create chart and elements
var container = document.getElementById('chart');
var values = document.getElementById('values');
var chart = new google.visualization.LineChart(container);
// wait until chart is ready
google.visualization.events.addListener(chart, 'ready', function () {
// initialize variables
var chartLayout = chart.getChartLayoutInterface();
var chartArea = chartLayout.getChartAreaBoundingBox();
var chartBounds = container.getBoundingClientRect();
var select = document.getElementById('select');
var x1 = 0;
var y1 = 0;
var x2 = 0;
var y2 = 0;
var x3 = 0;
var y3 = 0;
var x4 = 0;
var y4 = 0;
// listen for mouse events
window.addEventListener('mousedown', function (e) {
select.className = '';
x1 = e.pageX;
y1 = e.pageY;
reCalc();
});
window.addEventListener('mousemove', function (e) {
if (select.className === '') {
x2 = e.pageX;
y2 = e.pageY;
reCalc();
}
});
window.addEventListener('mouseup', function (e) {
select.className = 'static';
selectPoints();
});
// show user selection
function reCalc() {
x3 = Math.min(x1,x2);
x4 = Math.max(x1,x2);
y3 = Math.min(y1,y2);
y4 = Math.max(y1,y2);
select.style.left = x3 + 'px';
select.style.width = x4 - x3 + 'px';
select.style.top = (chartBounds.top + chartArea.top + window.pageYOffset) + 'px';
select.style.height = (chartArea.height + window.pageYOffset) + 'px';
}
// show values from selection
function selectPoints() {
if ((((chartBounds.left + window.pageXOffset) <= x3) &&
((chartBounds.left + chartBounds.width + window.pageXOffset) >= x4)) &&
(((chartBounds.top + window.pageYOffset) <= y3) &&
((chartBounds.top + chartBounds.height + window.pageYOffset) >= y4))) {
var rows = data.getFilteredRows([{
column: 0,
minValue: chartLayout.getHAxisValue(x3),
maxValue: chartLayout.getHAxisValue(x4)
}]);
values.innerHTML = '';
rows.forEach(function (index) {
var value = values.appendChild(document.createElement('div'));
value.innerHTML = data.getFormattedValue(index, 0) + ': ' + data.getFormattedValue(index, 1);
});
}
}
});
// draw chart
chart.draw(data, options);
});
#select {
background-color: #3366cc;
border: 1px solid #3366cc;
opacity: 0.2;
position: absolute;
z-index: 1000;
}
.hidden {
display: none;
visibility: hidden;
}
<script src="https://www.gstatic.com/charts/loader.js"></script>
<div class="hidden" id="select"></div>
<div id="chart"></div>
<div id="values"></div>
I am working on a project with D3.js that displays regions of interest (ROI) which are <g> elements with one <polygon> and one <text>. I noticed that zooming becomes very slow when there are a lot of ROI and it seems that this is mostly because of the texts, i.e. when they are hidden with display: none, zoom is much faster. The zoom speed is different in every browser: Firefox is quite fast, Chrome is average and Edge is slow.
I tried to speed up the text rendering by using the CSS property text-rendering: optimizeSpeed; but the difference is marginal. I noticed that some fonts are faster to render than others. Currently the best results I obtained is by using font-family: monospace;.
So my question is: How to zoom faster in an SVG drawing with D3? Is there a font that is known to be faster to render than others? Or is there a CSS, SVG or D3 trick that could help?
You can test the zoom speed with the snippet. If you click on the button, the text will be hidden an zooming will be much faster.
"use strict";
// Create data with this structure:
// DATA = [{ name: "", coords: [x0, y0, x1, y1, x2, y2, x3, y3]}]
var nbPolyX = 100;
var nbPolyY = 50;
var sqSize = 800 / nbPolyX;
var DATA = [];
for (let idY = 0; idY < nbPolyY; idY++) {
for (let idX = 0; idX < nbPolyX; idX++) {
DATA.push({
name: "x" + idX + "y" + idY,
coords: [
idX * sqSize + 1, idY * sqSize + 1,
(idX + 1) * sqSize - 1, idY * sqSize + 1,
(idX + 1) * sqSize - 1, (idY + 1) * sqSize - 1,
idX * sqSize + 1, (idY + 1) * sqSize - 1
]
})
}
}
var SVGELEM = {};
var ZOOMER = {};
var TRNSF = {
k: 1,
x: 0,
y: 0
};
var ZOOMER = {};
var GZOOM = {};
var ROI = {};
var POLY = {};
var TXT = {};
var BUTTON = {};
addButton();
addSVG();
function addSVG() {
ZOOMER = d3
.zoom()
.scaleExtent([0.9, 40])
.on("zoom", function () {
onZoom();
});
SVGELEM = d3.select("body").append("svg")
.attr("width", nbPolyX * sqSize)
.attr("height", nbPolyY * sqSize)
.call(ZOOMER);
GZOOM = SVGELEM.append("g");
ROI = GZOOM.selectAll("g")
.data(DATA)
.enter()
.append("g")
.classed("roi", true);
POLY = ROI.selectAll("polygon")
.data(function (d) {
return [d.coords];
})
.enter()
.append("polygon")
.attr("points", function (d) {
return d;
});
TXT = ROI.selectAll("text")
.data(function (d) {
var nbElem = d.coords.length;
// Polygon mean point X.
var xMean = 0;
for (let index = 0; index < nbElem - 1; index += 2) {
xMean += d.coords[index];
}
xMean /= nbElem / 2;
// Polygon mean point Y.
var yMean = 0;
for (let index = 1; index < nbElem; index += 2) {
yMean += d.coords[index];
}
yMean /= nbElem / 2;
// Return value.
var ret = {
name: d.name,
x: xMean,
y: yMean
};
return [ret];
})
.enter()
.append("text")
.attr("x", function (d) {
return d.x;
})
.attr("y", function (d) {
return d.y;
})
.text(function (d) {
return d.name;
});
}
function addButton() {
BUTTON = d3.select("body").append("button")
.text("HIDE TEXT")
.on("click", function btnOnClick() {
btnOnClick.state = !btnOnClick.state;
d3.selectAll("text").classed("cl_display_none", btnOnClick.state);
if (btnOnClick.state) d3.select(this).text("SHOW TEXT");
else d3.select(this).text("HIDE TEXT");
});
}
function onZoom() {
if (d3.event !== null) TRNSF = d3.event.transform;
GZOOM.attr(
"transform",
"translate(" + TRNSF.x + "," + TRNSF.y + ") scale(" + TRNSF.k + ")"
);
}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
<title>SVG ZOOM SPEED</title>
<style>
body {
text-align: center;
font-family: monospace;
display: block;
margin: 0 auto;
}
svg {
margin: 10px auto;
border: 1px solid;
display: block;
}
.roi polygon {
shape-rendering: optimizeSpeed;
vector-effect: non-scaling-stroke;
fill: rgba(0, 255, 0, 0.25);
stroke: rgba(0, 255, 0, 1);
stroke-width: 1px;
}
.roi text {
text-rendering: optimizeSpeed;
font-family: monospace;
font-size: 1px;
text-anchor: middle;
dominant-baseline: middle;
}
.cl_display_none {
display: none;
}
button {
width: 150px;
height: 50px;
font-family: monospace;
font-size: 15pt;
margin: 0;
}
</style>
</head>
<body>
<h3>SVG ZOOM SPEED</h3>
<p>Use the mouse wheel to zoom in and out the SVG drawing then hide the text with the button and observe the speed difference. Test it in different browsers.</p>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<script src="zoom_speed.js"></script>
</body>
</html>
So I have this similar svg image. white being transparent with black dots. I have to cover the whole background with this pattern and then target a 5x5square dot area of dots to change their color.
What is the simplest way or a common method to achieve this?
You can access elements of an SVG-image if it is embedded directly into the HTML-code. You can even create the whole SVG using JavaScript. Here is an example:
https://jsfiddle.net/da_mkay/eyskzpsc/7/
<!DOCTYPE html>
<html>
<head>
<title>SVG dots</title>
</head>
<body>
</body>
<script>
var makeSVGElement = function (tag, attrs) {
var element = document.createElementNS('http://www.w3.org/2000/svg', tag)
for (var k in attrs) {
element.setAttribute(k, attrs[k])
}
return element
}
var makeDotSVG = function (width, height, dotRadius) {
var svg = makeSVGElement('svg', { width: width, height: height, 'class': 'dot-svg' })
, dotDiameter = dotRadius*2
, dotsX = Math.floor(width / dotDiameter)
, dotsY = Math.floor(height / dotDiameter)
// Fill complete SVG canvas with dots
for (var x = 0; x < dotsX; x++) {
for (var y = 0; y < dotsY; y++) {
var dot = makeSVGElement('circle', {
id: 'dot-'+x+'-'+y,
cx: dotRadius + x*dotDiameter,
cy: dotRadius + y*dotDiameter,
r: dotRadius,
fill: '#B3E3A3'
})
svg.appendChild(dot)
}
}
// Highlight the hovered dots by changing its fill-color
var curMouseOver = function (event) {
var dotX = Math.floor(event.offsetX / dotDiameter)
, dotY = Math.floor(event.offsetY / dotDiameter)
, dot = svg.getElementById('dot-'+dotX+'-'+dotY)
if (dot !== null) {
dot.setAttribute('fill', '#73B85C')
}
}
svg.addEventListener('mouseover', curMouseOver)
return svg
}
// Create SVG and add to body
var myDotSVG = makeDotSVG(500, 500, 5)
console.log(document.body)
document.body.appendChild(myDotSVG)
</script>
</html>
I'm trying to display some circles based on their geo location with a slider (per day). The data is saved in a vorfaelle.json file which is here and the HTML/d3 file looks like this.
<!DOCTYPE html>
<head>
<title>D3 Mapping Timeline</title>
<meta charset="utf-8">
<link rel="stylesheet" href="d3.slider.css" />
<style>
path {
fill: none;
stroke: #333;
stroke-width: .5px;
}
.land-boundary {
stroke-width: 1px;
}
.county-boundary {
stroke: #ddd;
}
.site {
stroke-width: .5px;
stroke: #333;
fill: #9cf;
}
#slider3 {
margin: 20px 0 10px 20px;
width: 900px;
}
</style>
<script src="http://d3js.org/d3.v3.min.js"></script>
<script src="http://d3js.org/topojson.v1.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/underscore.js/1.8.3/underscore-min.js"></script>
<script src="d3.slider.js"></script>
</head>
<body>
<div id="slider3"></div>
<script>
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height);
var width = 1240,
height = 720;
var projection = d3.geo.mercator()
.translate([width / 2, height / 2])
.scale((width - 1) / 2 / Math.PI);
d3.json("vorfaelle.json", function(error, data){
console.log(data.features[1].geometry.coordinates);
window.site_data = data;
});
var displaySites = function(data) {
var sites = svg.selectAll(".site")
.data(data);
sites.enter().append("circle")
.attr("class", "site")
.attr("cx", function(d) {
for (var i = 0; i < d.features.length+1; i++) {
console.log(d.features[i].geometry.coordinates[0]);
return projection(d.features[i].geometry.coordinates[0])
//return projection([d.lng, d.lat])[0];
}
})
.attr("cy", function(d) {
for (var i = 0; i < d.features.length+1; i++) {
console.log(d.features[i].geometry.coordinates[1]);
return projection([d.features[i].geometry.coordinates[1]])
//return projection([d.lng, d.lat])[0];
}
})
.attr("r", 1)
.transition().duration(400)
.attr("r", 5);
sites.exit()
.transition().duration(200)
.attr("r",1)
.remove();
};
// var minDateUnix = moment('2014-07-01', "YYYY MM DD").unix();
// var maxDateUnix = moment('2015-07-21', "YYYY MM DD").unix();
var dateParser = d3.time.format("%d.%m.%Y").parse;
var minDate = dateParser("01.01.2015");
var maxDate = dateParser("31.12.2015");
console.log(minDate);
var secondsInDay = 60 * 60 * 24;
d3.select('#slider3').call(d3.slider()
.axis(true).min(minDate).max(maxDate).step(1)
.on("slide", function(evt, value) {
var newData = _(site_data).filter( function(site) {
for (var i = 0; i < site.features.length+1; i++) {
var date = dateParser(site.features[2].properties.date)
return date < value;
}
})
console.log("New set size ", newData.length);
displaySites(newData);
})
);
</script>
</body>
I am not sure if I am filtering the data properly at the end as I a was experimenting with this example and my data. When I move the slider I get this error:
For filtering you are do this which is wrong usage of filter as filter operates on an array.
var newData = _(site_data).filter( function(site) {
for (var i = 0; i < site.features.length+1; i++) {
var date = dateParser(site.features[2].properties.date)
return date < value;
}
})
You can do filtering like shown below:
d3.select('#slider3').call(d3.slider()
.axis(true).min(minDate).max(maxDate).step(1)
.on("slide", function(evt, value) {
newData = site_data.features.filter(function(d){
//convert the value to date
//convert the d.properties.date to date object using parser
return dateParser(d.properties.date) < new Date(value);
});
displaySites(newData);
})
);
Again in your code you doing a for loop to calculate the cx of the circle which is wrong:
.attr("cx", function(d) {
for (var i = 0; i < d.features.length+1; i++) {
console.log(d.features[i].geometry.coordinates[0]);
return projection(d.features[i].geometry.coordinates[0])
//return projection([d.lng, d.lat])[0];
}
})
There is no need for a for loop you should do like this:
.attr("cx", function(d) {
var p = projection(d.geometry.coordinates);
return p[0];
})
.attr("cy", function(d) {
var p = projection(d.geometry.coordinates);
return p[1]
})
Working code here
Hope this helps!
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 ?
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...
Purely data-driven
The data is updated and the viz manipulation left entirely to d3 declarations.
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.
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.
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.
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.
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.