Using D3.js, I have something like this:
var sets = [
{ data:[{date:1980,value:10},{date:1981,value:20},{date:1982,value:30}] },
{ data:[{date:1981,value:10},{date:1982,value:20},{date:1983,value:30}] },
{ data:[{date:1982,value:10},{date:1983,value:20},{date:1984,value:30}] }
];
And I bind it to make a chart like this:
var paths = g.selectAll("path")
.data(sets);
paths.enter()
.append("path")
.datum(function(d) { return d.data; })
.attr("class","line")
.attr("d", line);
Where g is a g element inside an svg element. This works. For each item in set I get a path using the values in data. Now what I want to do is click an element and replace the data with a different set:
var altData = [
{ data:[{date:1980,value:30},{date:1981,value:20},{date:1982,value:10}] },
{ data:[{date:1981,value:10},{date:1982,value:20},{date:1983,value:30}] },
{ data:[{date:1982,value:10},{date:1983,value:20},{date:1984,value:0}] }
];
d3.select("#transition").on("click", function() {
paths.data(altData);
console.log("click");
});
But the paths.data(altData) doesn't appear to do anything. There are no console errors, but the chart doesn't change. What do I need to do to tell it that the data has changed and the lines should be redrawn? As a bonus, I'd really like this transition to be animated.
Full fiddle
Basically you need to tell d3 to redraw it. In your case, it is by calling attr("d", line).
For transition, put transition() between two attr("d", fnc). Your onclick function will look like the following
d3.select("#transition").on("click", function() {
paths.attr("d", line)
.transition()
.attr("d", function(d, i){
return line(altData[i].data)
})
});
Jsfiddle http://jsfiddle.net/8fLufc65/
Look at this plnkr that will change the data when transition is clicked.
I made the part that draws the lines into a function and pass the data for which it should be drawing the lines.
drawPaths(sets) ;
function drawPaths(sets) {
var g = svg.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
var paths = g.selectAll("path")
.data(sets);
paths.enter()
.append("path")
.datum(function(d) { console.log(d); return d.data; })
.attr("class","line")
.attr("d", line);
}
Related
I have rewritten most of my d3 code to v4, but the new update pattern is throwing me off. The example below is for a force diagram. A duplicate circle is created within the first container upon every update. The data in my example does not actually change, but it's irrelevant. If I use new data, the same issue (a duplicate circle) occurs.
var w = 800,
h = 500;
var svg = d3.select("body").append("svg")
.attr("width", w)
.attr("height", h);
var dataset = {};
function setData() {
dataset.nodes = [{
value: 200
}, {
value: 100
}, {
value: 50
}];
}
setData();
var rScale = d3.scaleSqrt()
.range([0, 100])
.domain([0, d3.max(dataset.nodes.map(function(d) {
return d.value;
}))]);
var node = svg.append("g")
.attr("class", "nodes")
.attr("transform", "translate(" + w / 2 + "," + h / 2 + ")")
.selectAll(".node");
var simulation = d3.forceSimulation(dataset.nodes)
.force("charge", d3.forceManyBody().strength(-1600))
.force("x", d3.forceX())
.force("y", d3.forceY())
.alphaDecay(.05)
.on("tick", ticked);
function ticked() {
node.selectAll("circle")
.attr("cx", function(d) {
return d.x;
})
.attr("cy", function(d) {
return d.y;
});
}
function restart() {
// Apply the general update pattern to the nodes.
node = node.data(dataset.nodes, function(d) {
return d.id;
});
node.exit().remove();
node = node.enter().append("g")
.attr("class", "node")
.merge(node);
node.append("circle")
.attr("r", function(d) {
return rScale(d.value);
});
// Update and restart the simulation.
simulation.nodes(dataset.nodes);
simulation.alpha(1).restart();
}
restart();
function update() {
setData();
restart();
}
d3.select("#update").on("click", update);
If you click the Update button in this codepen (https://codepen.io/cplindem/pen/wpQbQe), you will see all three circles animate as the simulation restarts, but behind the largest circle, there is another, identical circle that does not animate. You can also see the new circle appear in the html if you inspect it.
What am I doing wrong?
Your first problem seems to be that you are keying the data on an 'id' field, but your data doesn't have any ids, so that needs changed or you just keep adding new groups:
function setData() {
dataset.nodes = [{
value: 200,
id: "A"
}, {
value: 100,
id: "B"
}, {
value: 50,
id: "C"
}];
console.log("dataset", dataset);
}
The second problem is you merge the new and updated selection and then append new circles to all of them, even the existing ones (so you have multiple circles per group on pressing update). I got it to work by doing this: make the new nodes, merge with existing selection, add circles to just the new nodes, update the circles in all the nodes:
node.exit().remove();
var newNodes = node.enter().append("g");
node = newNodes
.attr("class", "node")
.merge(node);
newNodes.append("circle");
node.select("circle")
.attr("r", function(d) {
return rScale(d.value);
});
Whether that 2nd bit is optimal I don't know, I'm still more anchored in v3 myself...
https://codepen.io/anon/pen/WdLexR
I've placed several pie charts on a map and want to adjust their size based on a corresponding value from a csv-file ("Total", in this example). But no matter how I adjust the radius, the pies won't show. Is there something important I missed?
My code so far:
d3.csv("Bevoelkerung-Altersstruktur-2010-Summe.csv", function drawPies (data) {
var pie = d3.layout.pie()
.sort(null)
.value(function(d) { return +d});
var arc = d3.svg.arc()
.innerRadius(0)
.outerRadius(function(d) {
return d.Total; });
var pies = svg.selectAll('.pie')
.data(data)
.enter()
.append('g')
.attr('class', 'pie')
.attr("transform", function(d) {
return "translate(" + projection([d.lon, d.lat])[0] + "," + projection([d.lon, d.lat])[1] + ")";
});
var color = d3.scale.ordinal()
.range(["#98abc5", "#7b6888", "#a05d56", "#d0743c",])
.domain(d3.range(0,4));
pies.selectAll('.slice')
.data(function(d){
return pie([d.Group1, d.Group2, d.Group3, d.Group4]); })
.enter()
.append('path')
.attr('d', arc)
.style('fill', function(d,i){
return color(i);
});
Here is the link to the complete code.
I could not run in a correct way your code so I moved a few things to get it working under a plnkr.
// You had all the async calls to remote data files nested which I
// recommend not doing. I separated your GeoJSON rendering and your
// pie rendering into two distinct functions.
// Start GeoJSON rendering
d3.csv("Jugendarbeitslosigkeit.csv", function(data) {
//Load in GeoJSON data
d3.json("PolenReg2.json", function(json) {
data.forEach(function(d, i) {
// ...more code
// This is a tricky part
// Since we separated the polygon and pie rendering
// and the polygon calls will take longer due to size
// the group containing the polygons will be rendered
// last, thus rendering the group after your pie group.
// This will make your pies stay behind the polygon paths
// that's why we use the insert. In order to position
// the polygons layer below the pies.
svg
.insert('g', ':first-child')
// ... more code
// End GeoJSON rendering
// Start Pie rendering
d3.csv("Bevoelkerung-Altersstruktur-2010-Summe.csv", function(err, data) {
// Set our large pie function
var pie = d3.layout.pie()
.sort(null)
.value(function(d) {
return +d.key;
});
// ... more code
// End Pie rendering
The important part is here:
var pies = svg
.append('g') // Add a group with the class 'big-pies'
.attr('class', 'big-pies')
.selectAll('.pie') // Join data to create groups by each polygon (as far as I understand)
.data(data)
.enter()
.append('g')
.attr('class', 'pie')
.attr("transform", function(d) {
var proj = projection([d.lon, d.lat]);
return "translate(" + proj[0] + "," + proj[1] + ")";
})
.selectAll('.slice') // Start pie - data join
.data(function(d) {
// set slice data with additional total value
// so that we can use this value at the attr d
// function
return pie([{
key: d.Kinder,
tot: d.total
}, {
key: d.Jugendliche,
tot: d.total
}, {
key: d.Erwachsene,
tot: d.total
}, {
key: d.Rentner,
tot: d.total
}]);
})
.enter()
.append('path')
.attr('d', function(d, i) {
// return the arc function with outer radius increased by the total value
return d3.svg.arc().innerRadius(0).outerRadius(d.data.tot * 2).call(d, d)
})
.style('fill', function(d, i) {
return c10(i);
});
Plnkr: https://plnkr.co/edit/CwiFnNmfIleo5zZ6BseW?p=preview
I'm attempting to draw a graph/network in which node contents should be filled with a third party library. So far, I was able to draw the graph with d3 with a container () for each node to be filled later on. My problem is that the containers seem not to yet exist when there are referred for drawing. Adding an onload event doesn't work either, but using a onclick event shows that everything is in place to work and most probably drawing starts before the DOM elements are actually created.
What is the best practice to make sure d3 generated DOM elements are created before starting assigning them content? The third party JS library I use only requires the div id to draw content in.
From arrays of nodes and relationships return by Neo4j, I build a graph with d3 as follow. Images to be displayed in nodes are stored in the neo4j data base as Base64 strings.
var width = 500, height = 500;
var div = d3.select("body").append("div")
.attr("class", "tooltip")
.style("opacity", 0);
var col = obj.columns;
var data = obj.data;
var nodes = obj.data[0][0].nodes;
var nodeMap = {};
nodes.forEach(function(x) { nodeMap[x.label] = x; });
var edges = obj.data[0][0].edges;
var links = edges.map(function(x) {
return { source: nodeMap[x.source], target: nodeMap[x.target], value: x.value };
});
var svg = d3.select("#graph").append("svg")
.attr("width", width)
.attr("height", height)
.attr("pointer-events", "all")
.append('g')
.call(d3.behavior.zoom().on("zoom", redraw))
.append('g');
var force = d3.layout.force()
.gravity(.3)
.distance(150)
.charge(-4000)
.size([width, height]);
var drag = force.drag()
.on("dragstart", dragstart);
force
.nodes(nodes)
.links(links)
.friction(0.8)
.start();
var link = svg.selectAll(".link")
.data(links)
.enter().append("line")
.attr("class", "link");
var node = svg.selectAll(".node")
.data(nodes)
.enter().append("svg:g")
.attr("class", "node")
.on("dblclick", dblclick)
.call(force.drag);
node.append("circle")
.attr("r", 50);
node.append("image")
// display structure in nodes
.attr("xlink:href", function(d){
if (d.imgB64) {
return 'data:image/png;base64, ' + d.imgB64 ;
}
})
.attr("x", -40)
.attr("y", -40)
.attr("width", 80)
.attr("height", 80);
force.on("tick", function() {
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.attr("transform", function(d) { return "translate(" + d.x + "," + d.y + ")"; });
});
That code works fine. Hence, what I'm attempting to do is to replace the Base64 images with canvas drawings generated by a third party library.
This library works as follow:
var myCanvas = newCanvasViewer(id, data);
This function knows how to generate an image from the given data and puts the result in the DOM element, if the element already exists in the DOM, or generate a new canvas element in the DOM, directly in but unfortunately erasing any other existing DOM elements.
So, I changed the d3 above code, replacing the node.append('image') block with:
node.append("canvas")
.attr("id", function(d) {
var cnv = newCanvasViewer(d.name, d.data);
return d.name;
});
This obviously doesn't work, canvas objects are displayed but not in the nodes, probably because the DOM element doesn't exist yet when calling newCanvasViewer. Furthermore, the d3 graph is overwritten.
When setting an onclick function calling newCanvasViewer, the drawing shows up within the nodes on click.
node.append("circle")
.attr("r", 50)
.attr("onclick", function(d) {
return "newCanvasViewer('"+d.name+"', '"+d.data+"')";
});
node.append("canvas")
.attr("id", function(d) {
return d.name;
});
Since I would like each node to display its canvas drawing from start, I was wondering when to call the newCanvasViewer function? I guess an oncreate function at each node would make it, but doesn't exist.
Would a call back function work? Should I call it once d3 is finished with the whole network drawing?
In order to be more comprehensive, here is the HTML and javascript code with callback function to attempt drawing canvas content once d3 d3 is done drawing. I'm still stuck, the canvas sample is actually displayed but no canvas content show in nodes, probably due to timing between generating canvas containers and using them.
HTML:
<body onload="init(); cypherQuery()">
<div id="graph" align="left" valign="top"></div>
<div id="sample">
<canvas id="mol" style="width: 160px; height: 160px;"></canvas>
</div>
</body>
Javascript (in the HTML header):
var xmlhttp;
function init() {
xmlhttp = new XMLHttpRequest();
}
function cypherQuery() {
// perform neo4j query, extract JSON and call the network drawing (d3).
// ...
var obj = JSON.parse(xmlhttp.responseText);
drawGraph(obj, drawCanvas);
// this is just to show that canvasViewer does the
// job if the <canvas id='...'> element exists in DOM
var nodes = obj.data[0][0].nodes;
var cnv = newCanvasViewer("sample", nodes[0].data);
}
function drawGraph(obj, drawCanvas) {
// see code above from...
var width = 500, height = 500;
// ... to
force.on("tick", function() {
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.attr("transform", function(d) {
return "translate(" + d.x + "," + d.y + ")";
});
});
function redraw() {
svg.attr("transform", "translate(" + d3.event.translate + ")" + " scale(" + d3.event.scale + ")");
// node.attr("font-size", (nodeFontSize / d3.event.scale) + "px");
}
function dblclick(d) {
d3.select(this).classed("fixed", d.fixed = false);
}
if (callback && typeof(callback) === "function") {
callback(nodes);
}
}
Following the County Bubbles example, it's easy to add a bubble for each county. This is how it is added in the example:
svg.append("g")
.attr("class", "bubble")
.selectAll("circle")
.data(topojson.feature(us, us.objects.counties).features
.sort(function(a, b) { return b.properties.population - a.properties.population; }))
.enter().append("circle")
.attr("transform", function(d) { return "translate(" + path.centroid(d) + ")"; })
.attr("r", function(d) { return radius(d.properties.population); })
.append("title")
.text(function(d) {
return d.properties.name
+ "\nPopulation " + formatNumber(d.properties.population);
});
However, rather than using a variable from the json file (population), I need to update the radii according to a variable which dynamically changes (so I cannot put it in the json file beforehand as was done in the example). I call updateRadii() when a county is clicked, which needs access to the FIPS.
var currFIPS,
flowByFIPS;
var g = svg.append("g");
queue()
.defer(d3.json, "us.json")
.defer(d3.csv, "census.csv", function(d) {
return {
work: +d.workplace,
home: +d.residence,
flow: +d.flow
}
})
.await(ready);
function ready(error, us, commute) {
// Counties
g.append("g")
.attr("class", "counties")
.selectAll("path")
.data(topojson.feature(us, us.objects.counties).features)
.enter().append("path")
.attr("d", path)
.on("click", function(d) {
// Get FIPS of selected county
currFIPS = d.id;
// Filter on selected county (i.e., grab
// people who work in the selected county)
var data = commute.filter(function(d) {
return d.work == currFIPS;
});
// Create d3.map for where these people live
flowByFIPS = d3.map(data, function(d) {
return d.home;
});
// Update radii at "home" counties to reflect flow
updateRadii();
});
// Bubbles
g.append("g")
.attr("class", "counties")
.selectAll("circle")
.data(topojson.feature(us, us.objects.counties).features)
.enter().append("circle")
.attr("id", function(d) { return d.id; })
.attr("transform", function(d) {
return "translate(" + path.centroid(d) + ")";
})
.attr("r", 0); // invisible before a county is clicked
}
function updateRadii() {
svg.selectAll(".counties circle")
.transition()
.duration(300)
.attr("r", function(d) {
return flowByFIPS.get(d.id).flow
});
}
According to the error code, I believe that the circles do not have an id (FIPS code) attached. How do I get them to have an id? (I tried nesting the circle with the path using .each as explained in this answer, but could not get it working.)
Note that the above code works for updating fill on paths (rather than circles). For example, sub updateRadii(); for updateFill(); with the function as:
function updateFill() {
svg.selectAll(".counties path")
.transition()
.duration(300)
.attr("fill", function(d) {
return flowByFIPS.get(d.id).color; // e.g., "#444"
});
}
The problem here is that you don't supply d3 with data in the update function. I will recommend you update the data loaded from the file on the clicks, and from there you update the svg.
var update = function() {
g.selectAll(".country")
.data(data)
.attr("r", function(d) { return d.properties.flow_pct });
};
var data = topojson.feature(us, us.objects.counties).features;
data.forEach(function(x) { x.properties.flow_pct = /* calc the value */; })
g.append("g")
.attr("class", "counties")
.selectAll(".country")
.data(data)
.enter()
.append("circle")
.attr("class", "country")
.attr("transform", function(d) {
return "translate(" + path.centroid(d) + ")";
})
.on("click", function(d) {
// more code
data.forEach(function(x) { x.properties.flow_pct = /* calc the NEW value */; })
update();
});
update();
I've tried to use as much as the same code as before, but still trying to straiten it a bit out. The flow is now more d3-like, since the update function works on the changed data.
Another plus which this approach is both first render and future updates use the same logic for finding the radius.
It turns out to be an obvious solution. I forgot to check for the cases where a flow doesn't exist. The code above works if updateRadii() is changed to:
function updateRadii() {
svg.selectAll(".counties circle")
.transition()
.duration(300)
.attr("r", function(d) {
if (currFIPS == d.id) {
return 0;
}
var county = flowByFIPS.get(d.id);
if (typeof county === "undefined") {
return 0;
} else {
return county.flow;
}
});
}
I've started from the multiline example http://bl.ocks.org/mbostock/3884955
I extended it to show the dots along the line, but I'm not able to give to the circle the same color of the line...
I'm really new in d3.js and I really need an advice.
here the example page: http://www.danielepennati.com/d3/linea.html
I've change some variable name to make the script more general so there is some difference with the original example code. the main one is the name of the variable that contain the mapped data: it is "column" and not "cities"
d3.tsv(surce_data, function(error, data) {
color.domain(d3.keys(data[0]).filter(function(key) { return key !== "id"; }));
var column = color.domain().map(function(name) {
return {
name: name,
values: data.map(function(d) {
return {id: d.id, value: +replace(d[name])};
})
};
});
the second main difference is the x axsis: in my code it is ordinal and not linear.
so to draw the line the code is:
var tracciato = svg.selectAll(".line-group")
.data(column)
.enter().append("g")
.attr("class", "line-group");
tracciato.append("path")
.attr("class", "line")
.attr("d", function(d) { return line(d.values); })
.style("stroke", function(d) { return color(d.name); });
to make the points along the line I've wrote this code:
var point = tracciato.append("g")
.attr("class", "line-point");
point.selectAll('circle')
.data(function(d,i){ return d.values})
.enter().append('circle')
.attr("cx", function(d, i) {
return x(i) + x.rangeBand() / 2;
})
.attr("cy", function(d, i) { return y(d.value) })
.attr("r", 5);
I'd link the points color be the same of the line, but the problem is the colors are assigned to the "column" object. I don't know how to give to each new circle within the same column the same column color...
I don't know if my problem is clear, please ask me if you need any more specification.
Thanks