While I'm on my way into learning the secrets of the beautiful world of Data Visualization, I'm encountering some difficulties with D3.js and the loading process of external data from a csv or a json file.
I'm kinda a newbie with JS so I'd like some help from experts.
Below my code:
var w = 500;
var h = 500;
// I'm setting up an empty array
var csvData = [];
//Create SVG element
var svg = d3.select("body")
.append("svg")
.attr("width", w)
.attr("height", h);
// Get the data
d3.csv("csv/cities.csv", function(dataset) {
for(var i = 0; i < dataset.length; i++) {
csvData.push(dataset[i]);
}
});
// Draw data
svg.select("body").selectAll("p")
.data(csvData)
.enter()
.append("p")
.text(function(d) { return d });
Well, I'm not sure I did understand well the correct way to load data and process these values. Can someone be so kind to give me an hint? I have access to the csvData array (using the console from the developers tools) but I can't see any data returned with the // Draw data section.
This is the csv file:
csv/cities.csv
city,state,population,land area
seattle,WA,652405,83.9
new york,NY,8405837,302.6
boston,MA,645966,48.3
kansas city,MO,467007,315.0
Thanks
The snippet below is an ajax call which loads csv/cities.csv asynchronously:
// Get the data
d3.csv("csv/cities.csv", function(dataset) {
for(var i = 0; i < dataset.length; i++) {
csvData.push(dataset[i]);
}
});
Thus the draw data section should have been like shown below:
// Get the data
d3.csv("csv/cities.csv", function(dataset) {
for(var i = 0; i < dataset.length; i++) {
csvData.push(dataset[i]);
}
// Draw data
svg.select("body").selectAll("p")
.data(csvData)
.enter()
.append("p")
.text(function(d) { return d });
});
Next Mistake:
You cannot add a p DOM inside SVG, that is the reason why you don't see the p DOM elements. I have appended the p DOM element into the body DOM that fixed the problem.
Working fiddle here
Related
How can I access/modify an data related SVG element in place? I got an interval function, which executes every 3 seconds to control the data.
window.setInterval(function() {
for (var i = 0; i < data.length; i++) {
if(data[i].active) {
//THIS SVG node.. .style("fill", "green")
} else {
//THIS SVG node.. .style("fill", "red")
}
}
}, 3000)
Or do I need to re-initialize the whole D3.js graph each time?
i'm using a rowchart to show the total of sales by item of a salesman.
Already tried a composite chart unsuccessfully like many posts from the google, but none of the examples uses a rowchart.
I need to do like the image, creating the red lines to represent the sale value target for each item, but i dont know how, can you guys help me? Thanks!
Actually this is my code to plot the rowchart
spenderRowChart = dc.rowChart("#chart-row-spenders");
spenderRowChart
.width(450).height(200)
.dimension(itemDim)
.group(totalItemGroup)
.elasticX(true);
Obviously you need a source for the target data, which could be a global map, or a field in your data.
I've created an example which pulls the data from a global, but it would also take from the data if your group reduction provides a field called target.
Then, it adds a new path element to each row. Conveniently the rows are already SVG g group elements, so anything put in there will already be offset to the top left corner of the row rect.
The only coordinate we are missing is the height of the rect, which we can get by reading it from one of the existing bars:
var height = chart.select('g.row rect').attr('height');
Then we select the gs and use the general update pattern to add a path.target to each one if it doesn't have one. We'll make it red, make it visible only if we have data for that row, and start it at X 0 so that it will animate from the left like the row rects do:
var target = chart.selectAll('g.row')
.selectAll('path.target').data(function(d) { return [d]; });
target = target.enter().append('path')
.attr('class', 'target')
.attr('stroke', 'red')
.attr('visibility', function(d) {
return (d.value.target !== undefined || _targets[d.key] !== undefined) ? 'visible' : 'hidden';
})
.attr('d', function(d) {
return 'M0,0 v' + height;
}).merge(target);
The final .merge(target) merges this selection into the main selection.
Now we can now animate all target lines into position:
target.transition().duration(chart.transitionDuration())
.attr('visibility', function(d) {
return (d.value.target !== undefined || _targets[d.key] !== undefined) ? 'visible' : 'hidden';
})
.attr('d', function(d) {
return 'M' + (chart.x()(d.value.target || _targets[d.key] || 0)+0.5) + ',0 v' + height;
});
The example doesn't show it, but this will also allow the targets to move dynamically if they change or the scale changes. Likewise targets may also become visible or invisible if data is added/removed.
thank you, due the long time to have an answer i've developed a solution already, but, really thank you and its so nice beacause its pretty much the same ideia, so i think its nice to share the code here too.
The difference its in my code i use other logic to clear the strokes and use the filter value of some other chart to make it dynamic.
.renderlet(function(chart) {
dc.events.trigger(function() {
filter1 = yearRingChart.filters();
filter2 = spenderRowChart.filters();
});
})
.on('pretransition', function(chart) {
if (aux_path.length > 0){
for (i = 0; i < aux_path.length; i++){
aux_path[i].remove();
}
};
aux_data = JSON.parse(JSON.stringify(data2));
aux_data = aux_data.filter(venda => filter1.indexOf(venda.Nome) > -1);
meta_subgrupo = [];
aux_data.forEach(function(o) {
var existing = meta_subgrupo.filter(function(i) { return i.SubGrupo === o.SubGrupo })[0];
if (!existing)
meta_subgrupo.push(o);
else
existing.Meta += o.Meta;
});
if (filter1.length > 0) {
for (i = 0; (i < Object.keys(subGrupos).length); i++){
var x_vert = meta_subgrupo[i].Meta;
var extra_data = [
{x: chart.x()(x_vert), y: 0},
{x: chart.x()(x_vert), y: chart.effectiveHeight()}
];
var line = d3.line()
.x(function(d) { return d.x; })
.y(function(d) { return d.y; })
.curve(d3.curveLinear);
var chartBody = chart.select('g');
var path = chartBody.selectAll('path.extra').data([extra_data]);
path = path.enter()
.append('path')
.attr('class', 'oeExtra')
.attr('stroke', subGruposColors[i].Color)
.attr('id', 'ids')
.attr("stroke-width", 2)
.style("stroke-dasharray", ("10,3"))
.merge(path)
path.attr('d', line);
aux_path.push(path);
}
}
})
And that's how it looks
I have this D3js line graph I'm working with.
I'd like to be able to save it as an image in the same directory as shown in this example
d3.select('#saveButton').on('click', function(){
var svgString = getSVGString(svg.node());
svgString2Image( svgString, 2*width, 2*height, 'png', save ); // passes Blob and filesize String to the callback
function save( dataBlob, filesize ){
saveAs( dataBlob, 'D3 vis exported to PNG.png' ); // FileSaver.js
function
}
});
no errors running the above
Although I am getting no errors, the file is not downloading when the button is clicked. What am I missing here? Thanks!
In your fiddle, you're appending g to your svg and assigning it your variable svg:
var svg = d3.select("#priceWithMilestones")
.append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.append("g")
.attr("transform",
"translate(" + margin.left + "," + margin.top + ")");
It looks like getSVGString() expects the root node, and not a g element. You should probably change your code so that svg reflects the root svg element, and create another variable to store the g element, but for a quick and dirty fix, you can change
var svgString = getSVGString(svg.node());
to
var svgString = getSVGString(d3.select('svg').node());
And should save. Updated fiddle: https://jsfiddle.net/c19664p3/8/
Edit: As for exported styles, it looks like you can't refer to selectors outside of the svg when declaring selecting. Additionally it looks like it has to consist of exactly just an id or a class. See my other answer for a more lax CSS rule exporter.
So changing this:
#priceWithMilestones .line {
fill: none;
stroke: #14da9e;
stroke-width: 2px;
}
to:
.line {
fill: none;
stroke: #14da9e;
stroke-width: 2px;
}
exports the line style for just the svg. Updated fiddle: https://jsfiddle.net/c19664p3/10/
After looking at getCSSStyles, it looks like it only checks for rules that match exactly the id or the class of the root svg and its child elements. I think this is a more forgiving implementation:
function getCSSStyles( parentElement ) {
var nodesToCheck = [ parentElement ], i;
// Add all the different nodes to check
var childNodes = parentElement.getElementsByTagName("*");
for (i = 0; i < childNodes.length; i++) {
nodesToCheck.push(childNodes[i]);
}
// Extract CSS Rules
var extractedCSSRules = [];
for (i = 0; i < document.styleSheets.length; i++) {
var s = document.styleSheets[i];
try {
if (!s.cssRules) continue;
} catch( e ) {
if (e.name !== 'SecurityError') throw e; // for Firefox
continue;
}
var cssRules = s.cssRules;
var ruleMatches;
for (var r = 0; r < cssRules.length; r++) {
ruleMatches = nodesToCheck.reduce(function (a, b) {
return a || b.matches(cssRules[r].selectorText);
}, false);
if (ruleMatches)
extractedCSSRules.push(cssRules[r].cssText);
}
}
return extractedCSSRules.join(' ');
}
You still need to use rules internal to the svg (e.g. you still need to change #priceWithMilestones .line to .line in the CSS), but for future projects, I think it should catch more elements.
Updated fiddle with all of the changes: https://jsfiddle.net/c19664p3/12/
I have the following function to draw different SVG shapes with D3.
updateChart(props){
var svg = d3.select(this.refs.svg);
let prefix = "XX";
// Render SVG Tags with ID and without attributes first
let objectsRender = svg.selectAll("line")
.data(props.data)
.enter()
.append(function(d) {
return document.createElementNS(
"http://www.w3.org/2000/svg",
d.objType);
})
.attr("id", function(d){ return prefix +d._id;})
.attr("class", "no-attr")
.on("click",(d)=>{this.modeClick(d);});
// Set Attributes for all classes with no attributes set so far
let objectsAttributes= svg.selectAll(".no-attr").each(function(){
let id= d3.select(this).attr("id");
let data = null;
data = props.data[0];
for(i = 0;i < props.data.length; i++){
if(id == prefix +props.data[i]._id){
data = props.data[i];
break;
}
}
if( data !== null){
d3.select(this)
.attr(data.coordinates)
.attr(data.style)
.attr("class", null);
}
}
);
}
props.data contains all the objects with the relevant svg data.
The problem I have right now, is that sometimes when this method is invoked,
D3 starts duplicating already existing SVG elements, even though the amount of data in props.data has not changed.
Can anyoe help me with that?
I would like to visualize a circle with a random colour at a random x and y coordinate, then add an extra colourful circle at a random location every second.
I am using d3.timer to run a function that appends x and y coordinates to my dataset object, which is bound to all of the circle objects. When I print the dataset object, I can see that my function does in fact append the new x and y coordinates to my dataset object. However, the visualization does not update with the new circles. How can I add a new circle every second?
Relevant functions below:
var reshuffleData = function(){
for (var i=0; i<5; i++){
console.log('Reshuffling')
dataset.push({x: randomXPosition(), y: randomYPosition()})
}
console.log(dataset)
return true
}
d3.timer(reshuffleData, 10);
Full jsfiddle here: http://jsfiddle.net/d74Le5xk/
It wasn't not working because d3.timer is used incorrectly. As d3.timer just takes a function to paint next animation frame. We do not control when the this function will be called but it will be called most likely (1/ frames per second seconds). Where FPS may change every second.
If you want to do something periodically use setInterval also you need to redraw the circles once the dataset size is changed.
Following is the jsfiddle link for the working code.
http://jsfiddle.net/d74Le5xk/3/
Also attaching the code here for reference.
HTML
<svg class='canvas'></svg>
Javascript
(function () {
var width = 420, height = 200;
var randomXPosition = function(d){
return Math.random() * width;
}
var randomYPosition = function(d){
return Math.random() * height;
}
var dataset = [];
var circleBatchSize = 5;
var maxCircleCount = 100;
for (var i=0; i < circleBatchSize; i++){
dataset.push({x: randomXPosition(), y: randomYPosition()})
}
var testInterval = null;
var reshuffleData = function(){
for (var i=0; i<circleBatchSize; i++){
dataset.push({x: randomXPosition(), y: randomYPosition()})
//return true;
}
console.log('Reshuffled ' + dataset.length)
console.log(dataset)
if(dataset.length > maxCircleCount) {
clearInterval(testInterval);
}
}
console.log(dataset);
var colours = ['#FDBB30', '#EE3124', '#EC008C', '#F47521', '#7AC143', '#00B0DD'];
var randomColour = function() {
return colours[Math.floor(Math.random() * colours.length)];
}
//d3.timer(reshuffleData, 0, 5000);
testInterval = window.setInterval(reshuffleData, 2000);
var canvas = d3.select('.canvas')
.attr('width', width)
.attr('height', height)
.style('background-color', 'black');
var datasetOldLength = 0;
function drawCircles() {
if(datasetOldLength === dataset.length ) {
return;
}
datasetOldLength = dataset.length;
var circles = canvas.selectAll('circle')
.data(dataset)
.enter()
.append('circle')
.style('r', 20)
.style('fill', randomColour)
.style('cx', function(d) { return d.x} )
.style('cy', function(d) { return d.y} );
if(dataset.length > maxCircleCount) {
return true;
}
}
d3.timer(drawCircles, 1000);
})();
d3.timer usage explanation
# d3.timer(function[, delay[, time]])
[function] argument is called at every frame rendering by d3. It's called until it returns true.
optional [delay] in milliseconds to delay the first invocation of the [function]. Delay is taken since the [time] passed in third argument. If [time] is not passed delay starts from new Date().getTime().
optional [time] is the epoch time from when the delay is considered.
Reference https://github.com/mbostock/d3/wiki/Transitions#timers