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.
Related
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!
I am plotting points on a UK map using D3 off a live data stream. When the data points exceed 10,000 the browser becomes sluggish and the animation is no longer smooth. So I modify the dataPoints array to keep only the last 5000 points.
However when I modify the dataPoints the first time using splice() D3 stops rendering any new points. The old points gradually disappear (due to a transition) but there are no new points. I am not sure what I am doing wrong here.
I have simulated the problem by loading data of a CSV as well storing it in memory and plotting them at a rate of 1 point every 100ms. Once the number of dots goes above 10 I splice to retain the last 5 points. I see the same behaviour. Can someone review the code and let me know what I am doing wrong?
Setup and the plotting function:
var width = 960,
height = 1160;
var dataPoints = []
var svg = d3.select("#map").append("svg")
.attr("width", width)
.attr("height", height);
var projection = d3.geo.albers()
.center([0, 55.4])
.rotate([4.4, 0])
.parallels([40, 70])
.scale(5000)
.translate([width / 2, height / 2]);
function renderPoints() {
var points = svg.selectAll("circle")
.data(dataPoints)
points.enter()
.append("circle")
.attr("cx", function (d) {
prj = projection([d.longitude, d.latitude])
return prj[0];
})
.attr("cy", function (d) {
prj = projection([d.longitude, d.latitude])
return prj[1];
})
.attr("r", "4px")
.attr("fill", "blue")
.attr("fill-opacity", ".4")
.transition()
.delay(5000)
.attr("r", "0px")
}
/* JavaScript goes here. */
d3.json("uk.json", function(error, uk) {
if (error) return console.error(error);
console.log(uk);
var subunits = topojson.feature(uk, uk.objects.subunits);
var path = d3.geo.path()
.projection(projection);
svg.selectAll(".subunit")
.data(subunits.features)
.enter().append("path")
.attr("class", function(d) { return "subunit " + d.id })
.attr("d", path);
svg.append("path")
.datum(topojson.mesh(uk, uk.objects.subunits, function(a,b) {return a!== b && a.id !== 'IRL';}))
.attr("d", path)
.attr("class", "subunit-boundary")
svg.append("path")
.datum(topojson.mesh(uk, uk.objects.subunits, function(a,b) {return a=== b && a.id === 'IRL';}))
.attr("d", path)
.attr("class", "subunit-boundary IRL")
svg.selectAll(".place-label")
.attr("x", function(d) { return d.geometry.coordinates[0] > -1 ? 6 : -6; })
.style("text-anchor", function(d) { return d.geometry.coordinates[0] > -1 ? "start": "end"; });
svg.selectAll(".subunit-label")
.data(topojson.feature(uk, uk.objects.subunits).features)
.enter().append("text")
.attr("class", function(d) { return "subunit-label " + d.id })
.attr("transform", function(d) { return "translate(" + path.centroid(d) + ")"; })
.attr("dy", ".35em")
.text(function(d) { return d.properties.name; })
// function applyProjection(d) {
// console.log(d);
// prj = projection(d)
// console.log(prj);
// return prj;
// }
lon = -4.6
lat = 55.45
dataPoints.push([lon,lat])
renderPoints()
});
Function to cleanup old points
var cleanupDataPoints = function() {
num_of_elements = dataPoints.length
console.log("Pre:" + num_of_elements)
if(num_of_elements > 10) {
dataPoints = dataPoints.splice(-5, 5)
}
console.log("Post:" + dataPoints.length)
}
Loading data from CSV and plotting at a throttled rate
var bufferedData = null
var ptr = 0
var renderNext = function() {
d = bufferedData[ptr]
console.log(d)
dataPoints.push(d)
ptr++;
renderPoints()
cleanupDataPoints()
if(ptr < bufferedData.length)
setTimeout(renderNext, 100)
}
d3.csv('test.csv', function (error, data) {
bufferedData = data
console.log(data)
setTimeout(renderNext, 100)
})
In the lines
points = svg.selectAll("circle")
.data(dataPoints)
points.enter() (...)
d3 maps each element in dataPoints (indexed from 0 to 5000) to the circle elements (of which there should be 5000 eventually). So from its point of view, there is no enter'ing data: there are enough circles to hold all your points.
To make sure that the same data point is mapped to the same html element after it changed index in its array, you need to use an id field of some sort attached to each of your data point, and tell d3 to use this id to map the data to elements, instead of their index.
points = svg.selectAll("circle")
.data(dataPoints, function(d){return d.id})
If the coordinates are a good identifier for your point, you can directly use:
points = svg.selectAll("circle")
.data(dataPoints, function(d){return d.longitude+" "+d.latitude})
See https://github.com/mbostock/d3/wiki/Selections#data for more details.
I'm following the given tutorial on D3
bar chart -2
I've setup my code in two functions one is init and one is update
var xScale = null;
var chart = null;
function init(w, c) {
xScale = d3.scale.linear()
.range([0, w]);
chart = d3.select(c)
.append('svg')
.attr('width', w);
function update(data) {
xScale.domain([0, d3.max(data, function(d) { return +d.value; })]);
chart.attr('height', 20 * data.length);
var bars = chart.selectAll('g')
.data(data);
bars.exit().remove();
bars.enter().append('g')
.attr('transform', function(d, i) { return 'translate(0,' + i * 20 + ')'; });
bars.append('rect')
.attr('width', function(d) { return xScale(+d.value); })
.attr('height', 18);
bars.append('text')
.attr('x', function(d) { return xScale(+d.value); })
.attr('y', 10)
.attr('dy', '.45em')
.text(function (d) { return d.name; });
}
When I call update first time, the bar chart is created correctly, on subsequenet update calls, it creates rect and text elements under tags instead of updating
My data is a dict {'name': a, 'value': 12, .....} The number of elements per update can be different. There might be same keys(names) with different values in each update
bars = chart.selectAll('g')
You are selecting all of the g elements (both new and existing).
bars.append('rect');
bars.append('text');
As a result, when you call append on bars, you are appending rect and text elements to both the new and existing g elements.
/* Enter */
enter = bars.enter().append('g');
enter.append('rect');
enter.append('text');
/* Update */
bars.attr('transform', function(d, i) {
return 'translate(0,' + i * 20 + ')';
});
bars.select('rect')
.attr('width', function(d) { return xScale(+d.value); })
.attr('height', 18);
bars.select('text')
.attr('x', function(d) { return xScale(+d.value); })
.attr('y', 10)
.attr('dy', '.45em')
.text(function (d) { return d.name; });
This allows you to append rect and text elements only to the enter selection, yet still allows you to update all the elements using your new data.
Note:
The enter selection merges into the update selection when you append or insert. Rather than applying the same operators to the enter and update selections separately, you can now apply them only once to the update selection after entering the nodes.
See: https://github.com/mbostock/d3/wiki/Selections#data
I'm trying to plot circles from data in my csv file, but the circles are not appearing on the svg canvas. I believe the problem stems from how I load in the data (it gets loaded as an array of objects), but I'm not quite sure how to figure out what to do next.
Based off this tutorial: https://www.dashingd3js.com/svg-text-element
D3.js code:
var circleData = d3.csv("files/data.csv", function (error, data) {
data.forEach(function (d) {
d['KCComment'] = +d['KCComment'];
d['pscoreResult'] = +d['pscoreResult'];
d['r'] = +d['r'];
});
console.log(data);
});
var svg = d3.select("body").append("svg")
.attr("width", 480)
.attr("height", 480);
var circles = svg.selectAll("circle")
.data(circleData)
.enter()
.append("circle");
var circleAttributes = circles
.attr("cx", function (d) { return d.KCComment; })
.attr("cy", function (d) { return d.pscoreResult; })
.attr("r", function (d) { return d.r; })
.style("fill", "green");
var text = svg.selectAll("text")
.data(circleData)
.enter()
.append("text");
var textLabels = text
.attr("x", function(d) { return d.KCComment; })
.attr("y", function(d) { return d.pscoreResult; })
.text(function (d) { return "( " + d.KCComment + ", " + d.pscoreResult + " )"; })
.attr("font-family", "sans-serif")
.attr("font-size", "20px")
.attr("fill", "red");
What the CSV looks like:
fmname, fmtype, KCComment, pscoreResult, r
test1, type1, 7.1, 8, 39
test2, type2, 1.2, 3, 12
You should have the circle-drawing code within the d3.csv function's callback, so it's only processed when the data is available.
d3.csv("data.csv", function (error, circleData) {
circleData.forEach(function (d) {
d['KCComment'] = +d['KCComment'];
d['pscoreResult'] = +d['pscoreResult'];
d['r'] = +d['r'];
});
console.log(circleData);
// Do the SVG drawing stuff
...
// Finished
});
Also note that instead of setting var circleData = d3.csv(... you should just define it in the callback function.
Here's a plunker with the working code: http://embed.plnkr.co/fzBX0o/preview
You'll be able to see a number of further issues now: both circles are overlapping and only one quarter is visible. That's because your KCComment and pscoreResult values used to define the circles' cx and cy are too small. Try multiplying them up so that the circles move right and down and are a bit more visible! Same is true of the text locations, but I'll leave those problems for you to solve
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");