Draw sprites from JPG with D3.js - javascript

I've got a scatter plot chart that consists of several hundred points, each point represented by an image. The coordinates as well as the image filenames are loaded via AJAX. It works as intended, but with this approach every single image is loaded by itself, resulting in several hundred requests. This is the code I'm using right now and that's working fine:
function position(dot) {
dot .attr("x", function(d) { return d.x; })
.attr("y", function(d) { return d.y; })
.attr("width", function(d) { return d.r * 5; })
.attr("height", function(d) { return d.r * 10; })
.attr("xlink:href", function(d) { return d.ImageFile })
;
}
So, to speed things up, I dynamically create a sprite image file on the server. This also works well. But I can't find out how to load that sprite and access the single images within that sprite. I came across clipPath, but I'm obviously using it incorrectly:
function position(dot) {
dot .attr("x", function(d) { return d.x; })
.attr("y", function(d) { return d.y; })
.attr("width", function(d) { return d.r * 5; })
.attr("height", function(d) { return d.r * 10; })
.attr("clipPath", function(d) { return d.i*140 + " 0 140 200"; })
.attr("xlink:href", function(d) { return 'mySprite.jpg'; })
;
}
So, how can I use a sprite image created dynamically on the server within my D3.js code?
My overall code is somewhat based on this example: https://github.com/mbostock/bost.ocks.org/blob/gh-pages/mike/nations/index.html

Related

How to coordinate interactions between multiple data visualizations, particularly when one of them uses nesting? JavaScript and D3

For a project I am attempting to have three visualizations for data based on car stats, where if you hover over one, the others will show the affects of that hovering as well.
The first is a bar graph, the second is a scatterplot, and the third is a line graph. For the line graph I wanted to group by manufacturer so that I don't have a couple hundred lines on my line graph, as the plot coordinates on the x and y are acceleration and model year. The other two don't need to be grouped in this way because one of their axes is the manufacturer.
I have the interactions from the line graph to the other two working since there is no nesting on the bar or scatterplot, and both the scatterplot and the bar graph can affect each other perfectly fine, but since the data is nested for the line graph, I can't seem to figure out how to access it, as the way I was doing it for the other two (using filtering) does not seem to work.
Below I am first showing where I am trying to create interactions when the mouse hovers (this is for the bar graph), and below that I include how my line graph is set up to show how it works. All I want is to make the corresponding line stand out more from the others by thickening the stroke when I hover over the bar or plot (in the scatterplot), and then go back to the normal size upon moving my cursor.
I followed the tutorial on the D3 website for line graphs, so there shouldn't be anything particularly wrong with that code.
Creating the bars for the bar graph, the mouseover and mouseout are the important parts:
var path1 = svg1.selectAll("myRect")
.data(data)
.enter()
.append("rect")
.attr("x", x1(0.1) )
.attr("y", function(d) { return y1(d.Manufacturer); })
.attr("height", y1.bandwidth() )
.attr("width", function(d) { return x1(d.Cylinders); })
.attr("fill", function (d) {
return color1(d.Cylinders);
})
.on('mouseover', function (d, i) {
svg1.selectAll('rect')
.filter(function(f) {
return f.Manufacturer === d.Manufacturer;
})
.attr("fill", function (d) {
return color4(d.Cylinders);
})
svg2.selectAll('circle')
.filter(function(f) {
return f.Manufacturer === d.Manufacturer;
})
.attr('r', 9)
.attr("fill", function (d) {
return color5(d.Horsepower);
});
svg3.selectAll('path') //THIS IS THE LINE GRAPH
.filter(function(f) {
console.log(this)
return ; // <-------This is where I don't know what to return to just get one line
})
.attr("stroke-width", 7)
})
.on('mouseout', function (d, i) {
svg1.selectAll('rect')
.filter(function(f) {
return f.Manufacturer === d.Manufacturer;
})
.attr("fill", function (d) {
return color1(d.Cylinders);
});
svg2.selectAll('circle')
.filter(function(f) {
return f.Manufacturer === d.Manufacturer;
})
.attr('r', 5)
.attr("fill", function (d) {
return color2(d.Acceleration);
});
d3.selectAll('path') //DELESLECTING LINE GRAPH
.filter(function(f) {
return f.key === d.Manufacturer; //this is what I tried before but it doesn't work
})
.attr("stroke-width", 1.5)
});
Creating the line graph:
var sumstat = d3.nest()
.key(function(d) { return d.Manufacturer;})
.entries(data);
// Add X axis
var x3 = d3.scaleLinear()
.domain([69, 84])
.range([ 0, width3 ]);
svg3.append("g")
.attr("transform", "translate(0," + height3 + ")")
.call(d3.axisBottom(x3).ticks(5));
// Add Y axis
var y3 = d3.scaleLinear()
.domain([8, d3.max(data, function(d) { return +d.Acceleration; })])
.range([ height3, 0 ]);
svg3.append("g")
.call(d3.axisLeft(y3));
var div3 = d3.select("#my_div").append("div")
.attr("class", "#tool_tip")
.style("opacity", 0)
.style("font-size", "xx-large");
// color palette
var res = sumstat.map(function(d){ return d.key }) // list of group names
var color = d3.scaleOrdinal()
.domain(res)
.range(['darkolivegreen','darkred','palevioletred','indianred', 'hotpink'])
// Draw the line
svg3.selectAll(".line")
.data(sumstat)
.enter()
.append("path")
.attr("fill", "none")
.attr("stroke", function(d){ return color(d.key) })
.attr("stroke-width", 1.5)
.attr("d", function(d){
return d3.line()
.x(function(d) { return x3(d.ModelYear); })
.y(function(d) { return y3(+d.Acceleration); })
(d.values)
})
.on('mouseover', function (d, i) {
//highlight;
svg3.selectAll("path")
.attr("stroke-width", 0.9)
d3.select(this)
.attr("stroke", function(d){ return color(d.key)})
.attr("stroke-width", 6)
svg1.selectAll('rect')
.filter(function(f) {
return f.Manufacturer === d.key;
})
.attr("fill", function (d) {
return color4(d.Cylinders);
})
svg2.selectAll('circle')
.filter(function(f) {
return f.Manufacturer === d.key;
})
.attr('r', 9)
.attr("fill", function (d) {
return color5(d.Horsepower);
});
})
.on('mouseout', function (d, i) {
svg3.selectAll("path")
.attr("stroke-width", 1.5)
d3.select(this)
.attr("stroke", function(d){ return color(d.key)})
.attr("stroke-width", 1.5)
svg1.selectAll('rect')
.filter(function(f) {
return f.Manufacturer === d.key;
})
.attr("fill", function (d) {
return color1(d.Cylinders);
})
svg2.selectAll('circle')
.filter(function(f) {
return f.Manufacturer === d.key;
})
.attr('r', 5)
.attr("fill", function (d) {
return color2(d.Horsepower);
});
});
Any assistance I can get would be greatly appreciated!!
I think I may have figured out the problem. It would seem that trying to filter the paths causes an issue because the x and y axes are also technically lines, and thus have paths that are null. I tried
svg3.selectAll('path')
.filter(function(f) {
console.log(f)
if(f!=null)
return f.key === d.Manufacturer;
})
.attr("stroke-width",7)
In the .on('mouseover') function, and it seems to be working. The issue was the nulls, not the actual accessing of the keys.
Still taking suggestions if there is a better way to do this!

How to show circles (created using JSON data) in reverse order using a delay function?

I'm trying to create a dynamic visualization using circles that 'spread' out over some amount of time and all of the circles have the same centerpoint.
I have a separate script that creates the circles and stores the data in a JSON file - the first circle in the JSON file is the smallest circle on top of the image linked above.
Please see code snippet below. Basically, the script appends the circle data into circles into an svg with visibility set to none. The script reveal the circles one by one.
In the appending function, I tried using the .lower() function to reverse the order that the circles are appended to the svg because if I were to append in the order that the JSON file is in, each consecutive circle would hide the one below it. But then the animation plots backwards, where the larger circle plots first.
In the revealing function, I then tried adding a similar '.lower()' function to the transition method so each consecutive circle would reveal behind the previously revealed circle but then the code breaks. I'm just at a loss here - any pointers would be much appreciated.
html,
body,
#svg {
background-color: #FFFFFF;
}
<html>
<head>
<meta charset="utf-8" />
<title>Visualizer</title>
</head>
<body>
<div>
<button onclick="plotStatically(0, 0, 'testingcircle.json')">Draw Static ►</button>
<button onclick="plotConsecutively(0, 0, 'testingcircle.json')">Draw Dynamic ►</button>
</div>
<script src="https://d3js.org/d3.v5.min.js" charset="utf-8"></script>
<script>
function plotConsecutively(x, y, nameFile) {
d3.json(nameFile).then(function(data) {
var svgHeight = window.innerHeight - 100;
var svgWidth = window.innerWidth - 10;
var svg = d3.select('body').append('svg')
.attr('width', svgWidth)
.attr('height', svgHeight);
svg.selectAll("circle")
.data(data)
.enter()
.append('circle')
.attr('r', function(d) {
return d.r;
})
.attr('cx', function(d) {
return d.cx + x;
})
.attr('cy', function(d) {
return d.cy + y;
})
.attr('fill', function(d) {
return d.fill;
})
.attr('visibility', 'hidden')
svg.selectAll("circle")
.transition()
.delay(function(d, i) {
return 3.4 * i;
})
.duration(10)
.attr('visibility', 'visible');
})
}
function plotStatically(x, y, nameFile) {
d3.json(nameFile).then(function(data) {
var svgHeight = window.innerHeight - 100;
var svgWidth = window.innerWidth - 10;
var svg = d3.select('body').append('svg')
.attr('width', svgWidth)
.attr('height', svgHeight);
svg.selectAll("circle")
.data(data)
.enter()
.append('circle')
.attr('r', function(d) {
return d.r;
})
.attr('cx', function(d) {
return d.cx;
})
.attr('cy', function(d) {
return d.cy;
})
.attr('fill', function(d) {
return d.fill;
});
})
}
</script>
</body>
</html>
I think you were pretty much there.
As you said, the larger circles need to be appended to the svg first so that they don't block out the smaller circles beneath them. I think this is most easily done simply by reversing the order of the data array just after you get the results of the json file:
d3.json(nameFile).then(function(data) {
data = data.reverse();
...
Then, in order to show the circles from the inside out, you can change your delay function so that the items at the end of the array that you want to show first (the smaller circles) have the smallest delay, and the items at the beginning of the array that you want to show last (the larger circles) have the largest delay.
The third argument to the delay() function is the NodesList containing all the selected DOM elements, so you can use the length property of that array in your calculations.
...
.delay(function(d, i, circleNodes) {
return 3.4 * ((circleNodes.length - 1) - i);
})
...
let data = [
{"r":5,"cx":100,"cy":100,"fill":"red"}, {"r":10,"cx":100,"cy":100,"fill":"magenta"},{"r":15,"cx":100,"cy":100,"fill":"orange"},{"r":20,"cx":100,"cy":100,"fill":"green"},{"r":25,"cx":100,"cy":100,"fill":"blue"}
];
data = data.reverse();
function plotConsecutively() {
var svg = d3.select('#svg')
.append('svg')
.attr('width', 200)
.attr('height', 200);
svg.selectAll("circle")
.data(data)
.enter()
.append('circle')
.attr('r', function(d) {
return d.r;
})
.attr('cx', function(d) {
return d.cx;
})
.attr('cy', function(d) {
return d.cy;
})
.attr('fill', function(d) {
return d.fill;
})
.attr('visibility', 'hidden')
svg.selectAll('circle')
.transition()
.delay(function(d, i, nodes) {
return 150 * ((nodes.length - 1) - i);
})
.duration(10)
.attr('visibility', 'visible');
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<button onclick="plotConsecutively()">Draw Dynamic ►</button>
<div id="svg"></div>

Appending to an SVG Group from a second data source

I am trying to work on a project where I need to generate rows of grouped data and draw dots on based on Json data from two different files.
I need to place a series of dots initially and then add another series later upon a button push. For testing purposes I have two Json files: one for sales and one for Buys. Each file has two customers with nested data for sales or buys. I group by Company, drawing a red dot for each sale, with this code. This works very well:
function loadSVG() {
//Load in GeoJSON data
//d3.json("data/zzMonthlySalesAndBuysComb.json", function (json) {
d3.json("data/zzMonthlySales2.json", function (json) {
g = svg.append('g').classed("chart", true)
.attr("width", w)
.selectAll(".csMove")
.data(json, function (d) { return d.CompanyName + d.Company; })
.enter()
.append("g")
.classed("csMove", true)
//.attr({ width: w, height: 100 })
//.attr("transform", function (d, i) {
//return "translate(0," + h / 2 + ")";
//})
.attr("transform", function (d, i) { return "translate(0, " + i * 100 + ")"; })
.append("g")
.classed("CustomerBox", true);
//This test code
g.append("rect")
.attr("width", w)
.attr("height", function (d) { return h / 2; })
.style("fill", "silver");
var SalesDot = svg.selectAll(".CustomerBox").selectAll(".Sdot")
//.data(json)
.data(function (d) { return d.monthlySales })
.enter()
.append("g")
.classed("Sdot", true);
//then we add the circles in the correct company group
SalesDot
.append("circle")
.attr("cx", function (d) { return ((d.month - 20130001) / 2); })
.attr("cy", function (d) { return d.sales })
.attr("r", 5)
.style("fill", "red");
//Test - add dots initially
});
}
This works great. but this is where it fails. I have a button on the page and when I press the button I run this function which will load the buys data, I just get just two green dots each at the 0, 0 coordinates of the two groups.
function AddToSVG() {
//Load in GeoJSON data
//d3.json("data/zzMonthlyBuys2.json", function (json2) {
d3.json("data/zzMonthlyBuys2.json", function (json2) {
//add Green Circles.
var BuysDot = svg.selectAll(".CustomerBox").selectAll(".Bdot")
.data(json2)
//.data(function (d) { return d.monthlySales })
.enter()
.append("g")
.classed("Bdot", true);
//then we add the circles in the correct company group
BuysDot
.data(function (d) {
return d.monthlyBuys;
})
.enter()
.append("circle")
.attr("cx", function (d) {
return ((d.monthBuys - 20130001) / 2);
})
.attr("cy", function (d) { return d.buys })
.attr("r", 5)
.style("fill", "green");
});
}
Specifically what is happening is that the system still sees d as having data from monthlySales rather than MonthlyBuys. I see this when I put a break point at return d.monthlyBuys.
Does anyone know how I can fix this? I need the Buys and MonthlyBuys to be drawn over the existing groups for the correct Customers.

Integrating d3's fisheye plugin with a force layout tick function

I'm trying to apply the fisheye effect to a force layout with collision detection, node dragging, zoom etc but am struggling to integrate a fisheye with my tick function.
The closest existing example I found was http://bl.ocks.org/bentwonk/2514276 but it doesn't really contain a custom tick function or enable the usual things like dragging a node while fisheye is applied. For the sake of clarity, here's my tick function regardless of fisheye...
function tick(e) {
link.attr("x1", function(d) { return d.source.x;})
.attr("y1", function(d) { return d.source.y;})
.attr("x2", function(d) { return d.target.x;})
.attr("y2", function(d) { return d.target.y;})
node.each(gravity(.2 * e.alpha))
.each(collide(jitter))
.attr("transform", function(d) {
return "translate(" + d.x + "," + d.y + ")";
});
}
And here's what I'm hoping to use on my svg listener to trigger fisheye...
svg.on("mousemove", function() {
fisheye.focus(d3.mouse(this));
node
.each(function(d) { d.fisheye = fisheye(d); })
.attr("cx", function(d) { return d.fisheye.x; })
.attr("cy", function(d) { return d.fisheye.y; })
.attr("r", function(d) { return d.fisheye.z * 8; });
link.attr("x1", function(d) { return d.source.fisheye.x; })
.attr("y1", function(d) { return d.source.fisheye.y; })
.attr("x2", function(d) { return d.target.fisheye.x; })
.attr("y2", function(d) { return d.target.fisheye.y; });
});
But when using both together I notice no fisheye effect - presumably tick() is overriding any changes from my svg listener (although even if I add force.stop() to my svg listener I still see no fisheye on the page or any console errors).
And of course if I replace the tick function, then the whole node layout won't compute properly to start with.
Apologies that I don't have a more specific question, but does anyone have any thoughts on the best way to approach combining these 2 behaviours so that I can use a fisheye effect without compromising on other force layout functionality?
Thanks!
EDIT:
My only thought so far is to simply use the svg listener to trigger the mouse position...
svg.on("mousemove", function() {
//force.stop();
fisheye.focus(d3.mouse(this));
});
And then have some kind of coordinate addition in the tick function...
node.each(gravity(.2 * e.alpha))
.each(collide(jitter))
.each(function(d) { d.fisheye = fisheye(d); })
.attr("r", function(d) { return d.fisheye.z * 8; })
.attr("transform", function(d) {
return "translate(" + d.x + d.fisheye.x + "," + d.y + d.fisheye.y + ")";
});
(the above doesn't work though - just produces invalid coordinates so all nodes end up at the top-left of the screen)

Aligning text to the left & Right of a circle depending on the data

Hi I was wondering anyone knew a suitable way of aligning text on the right or left of a circle dependent on the data it holds.
At the moment I have this & it works:
//creating the circles & text together
var node = svg.selectAll("a.node")
.data(json)
.enter().append("a")
.attr("class", "node")
;
//returning the shape & colors variable & linking it with the relevance in DB
node.append('path')
.attr("d", d3.svg.symbol().type(circle))
.attr("transform", "translate(0,0)")
;
//returning the names from the db
node.append("text")
.text(function(d) { return d.Name; })
.style("font-size", "8px")
.style("font", "Arial")
.attr('x', function (d) {
if (d.Field === "Canda"&& d.Name.length > 40) {return -180 }
else if (d.Field === "Canda") {return 9}
else {return 2}
;})
.attr('y', 2);
But, due to my json text being different lengths - when I say '-140' the texts that are shorter aren't even close to the circle. Therefore, is there a dynamic way to have the text all the same distance from the circle no matter if the length varies?
EDIT: just added .length .. but that would still mean that I would need to do more if statements as the text would be varied lengths.
http://jsfiddle.net/xwZjN/66/
A solution involving text-anchor:
force.on("tick", function() {
// edit to include text-anchor
text.attr("text-anchor", function(d) {
if( d.Country ==="USA" ) { return "end" }
else { return "start" }
} )
// edit to change x starting point
text.attr("x", function(d) {
if (d.Country ==="USA") {
return d.x - 10;
}
else {
return d.x + 6;
}
})
.attr("y", function(d) { return d.y + 4; });
node.attr("cx", function(d) { return d.x; })
.attr("cy", function(d) { return d.y; });
});

Categories

Resources