I'm trying to use attrTween in d3 to animate a pie chart when the page is loaded but it's not working for me. I've used attrTween before to animate a change in data and it's worked fine but this time I want to 'grow' the pie chart when the page is loaded first but it's not behaving as expected and I'm not getting any information as to why this is.
If I remove the line .attrTween('d' arcTweenStart); then everything works fine except of course it does not animate. If the line is left in then nothing is displayed and the arcTweenStart function is never entered. Can anyone spot where I'm going wrong?
function drawCharts()
{
// Create the chart and bind the data to it and position it
var pieChart = d3.select("#groupRisk").selectAll("svg")
.data(dataSet) // Bind the data to the chart
.enter().append("svg")
.attr("id", "pie")
.attr("width", w) // Set th width
.attr("height", h) // Set the height
.append("g")
.attr("transform", "translate(" + radius + "," + radius + ")"); // Position the chart
// Create the pie chart layout
var pie = d3.layout.pie()
.value(function(d) { return d.count; })
.sort(null); // Sort is set to null to allow for better looking tweens
// Create "slices" for each data element
var arcs = pieChart.selectAll("g.slice")
.data(pie) // Bind the pie layout to the slices
.attr("id", "arcs")
.enter()
.append("g")
.attr("class", "slice");
// Create the graphics for each slice and colour them
arcs.append("path")
.attr("fill", function(d, i) { return color(i); })
.attr("d", arc)
.each(function(d) { this._current = d; })
.transition()
.duration(500)
.attrTween('d' arcTweenStart);
}
function arcTweenStart(b)
{
var start =
{
startAngle: b.startAngle,
endAngle: b.endAngle
};
var i = d3.interpolate(start, b);
return function(t)
{
return arc(i(t));
};
}
EDIT:
My data set looks like this:
var dataSet=
[
[
{ "label": "Green", "count": 40 },
{ "label": "Amber", "count": 50 },
{ "label": "Red", "count": 10 }
],
[
{ "label": "Green", "count": 20 },
{ "label": "Amber", "count": 30 },
{ "label": "Red", "count": 50 }
],
[
{ "label": "Green", "count": 50 },
{ "label": "Amber", "count": 20 },
{ "label": "Red", "count": 30 }
]
];
I have an array of data sets so I want to draw a chart for each one.
You don't show what your dataSet variable holds (that would have really helped answer the question!) but assuming your data looks like this:
var dataSet = [{
count: 4
}, {
count: 5
}, {
count: 6
}];
You don't need to do the first bind/enter:
d3.select("#groupRisk").selectAll("svg")
.data(dataSet) // Bind the data to the chart
.enter()
...
This would give you a pie chart for each entry in the data. Getting rid of that, your bind then becomes:
var arcs = pieChart.selectAll("g.slice")
.data(pie(dataSet)) //<-- call pie with the dataSet
.attr("id", "arcs")
.enter()
.append("g")
.attr("class", "slice");
But really to the heart of your question, your tween var start, has the same start/end angle as where you want to end. So, you animate the same thing over and over again. What I think you meant is:
function arcTweenStart(b) {
var start = {
startAngle: b.startAngle,
endAngle: b.startAngle //<-- set end to start and adjust on each call
};
var i = d3.interpolate(start, b);
return function(t) {
return arc(i(t));
};
}
Oh, and one typo in there too:
.attrTween('d' arcTweenStart); //<-- comma missing between 'd' and arcTweenStart
Example here.
Related
I would like to create a graphic in D3 that consists of nodes connected to each other with curved lines. The lines should be curved differently depending on how far apart the start and end point of the line are.
For example (A) is a longer connection and therefore is less curved than (C).
Which D3 function is best used for this calculation and how is it output as SVG path
A code example (for example on observablehq.com) would help me a lot.
Here is a code example in obserbavlehq.com
https://observablehq.com/#garciaguillermoa/circles-and-links
I will try to explain it, let me know if there is something I am not clear enough:
Lets start with our circles, we use d3.pie() to position this circles, passing the data defined above, it will return us some arcs, but as we want circles instead of arcs, we use arc.centroid to get the coordinates of our circles
Value is required for the spacing in the pie layout that we use to calculate the position, if you want more circles, you will need to reduce the value, here is the related code:
pie = d3
.pie()
.sort(null)
.value((d) => {
return d.value;
});
arc = d3.arc().outerRadius(300).innerRadius(50);
data = [
{ id: 0, value: 10 },
{ id: 1, value: 10 },
{ id: 2, value: 10 },
{ id: 3, value: 10 },
{ id: 4, value: 10 },
{ id: 5, value: 10 },
{ id: 6, value: 10 },
{ id: 7, value: 10 },
{ id: 8, value: 10 },
{ id: 9, value: 10 },
];
const circles = [];
for(let item of pieData) {
const [x, y] = arc.centroid(item);
circles.push({x, y});
}
Now we can render the circles:
const svg = d3.create("svg")
.attr("width", width)
.attr("height", height);
const mainGroup = svg
.append("g")
.attr("id", "main")
.attr("transform", "translate(" + width / 2 + "," + height / 2 + ")");
// Insert lines and circles groups, lines first so they are behind circles
const linesGroup = mainGroup.append("g").attr("id", "lines");
const circlesGroup = mainGroup.append("g").attr("id", "circles");
circlesGroup
.selectAll("circle")
.data(circles, (_, index) => index)
.join((enter) => {
enter
.append("circle")
.attr("id", (_, index) => {
return `circle-${index}`;
})
.attr("r", 20)
.attr("cx", (d) => {
return d.x;
})
.attr("cy", (d) => {
return d.y;
})
.style("stroke-width", "2px")
.style("stroke", "#000")
.style("fill", "#963cff");
});
Now we need to declare the links, we could do this with an array specifying the id of the source and destination (from and to). we use this to search each circle, get its coordinates (the source and destination of our links) and then create the links, in order to create them, we can use a path and the d3 method quadraticCurveTo, this function requires four parameters, the first two are "the control point" which defines our curve, we use 0, 0 as it is the center of our viz (it is the center because we used a translate in the parent group).
lines = [
{
from: 1,
to: 3,
},
{
from: 8,
to: 4,
},
];
for (let line of lines) {
const fromCircle = circles[line.from];
const toCircle = circles[line.to];
const fromP = { x: fromCircle.x, y: fromCircle.y };
const toP = { x: toCircle.x, y: toCircle.y };
const path = d3.path();
path.moveTo(fromP.x, fromP.y);
path.quadraticCurveTo(0, 0, toP.x, toP.y);
linesGroup
.append("path")
.style("fill", "none")
.style("stroke-width", "2px")
.style("stroke-dasharray", "10 10")
.style("stroke", "#000")
.attr("d", path);
}
I am trying to make a map of meteorite landings across the world using D3 v5. I have the map displaying. The coordinates (lat, long) from the meteorite json file are loading. I am trying to use them in .attr for "cx" and "cy". When I console.log the coordinates in .attr, they show up, but when I try to pass them through my projection so they will display properly on the map, I am getting the following error: Uncaught (in promise) TypeError: Cannot read property 'coordinates' of null.
Can anyone help me figure out how to get this working? Appreciate any help you can offer.
Here a link to a Codepen: https://codepen.io/lieberscott/pen/QryZPR?editors=0110
And my code:
const w = 960;
const h = 600;
const svg = d3.select(".map")
.append("svg")
.attr("height", h)
.attr("width", w);
let projection = d3.geoMercator()
.translate([w/2, h/2])
.scale(140);
const path = d3.geoPath()
.projection(projection);
let tooltip = d3.select("body")
.append("div")
.attr("class", "tooltip");
const files = ["https://unpkg.com/world-atlas#1.1.4/world/110m.json", "https://raw.githubusercontent.com/FreeCodeCamp/ProjectReferenceData/master/meteorite-strike-data.json"];
Promise.all(files.map((url) => d3.json(url))).then(function(data) {
svg.append("g")
.attr("class", "country")
.selectAll("path")
.data(topojson.feature(data[0], data[0].objects.countries).features)
.enter().append("path")
.attr("d", path);
svg.selectAll(".meteor")
.data(data[1].features)
.enter().append("circle")
.attr("class", "meteor")
.attr("cx", (d) => {
console.log(d.geometry.coordinates[0]);
let coords = projection([d.geometry.coordinates[0], d.geometry.coordinates[1]]);
return coords[0];
})
.attr("cy", (d) => {
let coords = projection([d.geometry.coordinates[0], d.geometry.coordinates[1]]);
return coords[1];
})
.attr("r", 6);
});
Your data is missing coordinates for certain locations, eg:
{
"type": "Feature",
"geometry": null,
"properties": {
"mass": "2250",
"name": "Bulls Run",
"reclong": null,
"geolocation_address": null,
"geolocation_zip": null,
"year": "1964-01-01T00:00:00.000",
"geolocation_state": null,
"fall": "Fell",
"id": "5163",
"recclass": "Iron?",
"reclat": null,
"geolocation_city": null,
"nametype": "Valid"
}
},
This generates the error you see, stopping the appending of circles.
You could try to filter them out with something like:
svg.selectAll(".meteor")
.data(data[1].features.filter(function(d) {
return d.geometry; // return only features with a geometry
}) )
Giving us:
Updated pen: https://codepen.io/anon/pen/XqXQYy?editors=0110
Also, I'll quickly note that this:
projection([d.geometry.coordinates[0], d.geometry.coordinates[1]]);
Can be simplified to this:
projection(d.geometry.coordinates);
As per below code I am expecting bounce effect when the pie chart loads for the first time which do not work as expected and expand the arc slice when on mouseenter but slicing the selected arc overlaps the adjacent arcs while it should work as red arc as in the below example it should only expand and displace other arcs. Can any give pointer on where exactly I am doing wrong.
Pie Chart
var width = 960,
height = 500,
radius = Math.min(width, height) / 2 - 10;
var data=[
{
"age": "<5",
"population": 2704659
},
{
"age": "5-13",
"population": 4499890
},
{
"age": "14-17",
"population": 2159981
},
{
"age": "18-24",
"population": 3853788
},
{
"age": "25-44",
"population": 14106543
},
{
"age": "45-64",
"population": 8819342
},
{
"age": "≥65",
"population": 612463
}
];
var color = d3.scale.category20();
var arc = d3.svg.arc()
.outerRadius(radius);
var pie = d3.layout.pie()
.sort(null)
.value(function(d) { return d.population; });
var labelArc = d3.svg.arc()
.outerRadius(radius - 40)
.innerRadius(radius - 40);
var svg = d3.select("body").append("svg")
.datum(data)
.attr("width", width)
.attr("height", height)
.append("g")
.attr("transform", "translate(" + width / 2 + "," + height / 2 + ")");
var arcs = svg.selectAll("g.arc")
.data(pie)
.enter().append("g")
.attr("class", "arc");
arcs.append("path")
.attr("fill", function(d, i) { return color(i); }).on("mouseenter", function(d) {
var endAngle = d.endAngle + 0.2;
var startAngle = d.startAngle - 0.2;
var arcOver = d3.svg.arc()
.outerRadius(radius + 10).endAngle(endAngle).startAngle(startAngle);
d3.select(this)
.attr("stroke","white")
.transition()
.ease("bounce")
.duration(1000)
.attr("d", arcOver)
.attr("stroke-width",6);
})
.on("mouseleave", function(d) {
d3.select(this).transition()
.attr("d", arc)
.attr("stroke","none");
})
.transition()
.ease("bounce")
.duration(2000)
.attrTween("d", tweenPie).attr("d", arc);
function tweenPie(b) {
b.innerRadius = 0;
var i = d3.interpolate({startAngle: 0, endAngle: 0}, b);
return function(t) { return arc(i(t)); };
}
arcs.append("text")
.attr("transform", function(d) { return "translate(" + labelArc.centroid(d) + ")"; })
.attr("dy", ".35em")
.text(function(d) { return d.data.age; });
function type(d) {
d.population = +d.population;
return d;
}
</script>
Here is the bin of what I have tried so far.
The arcs overlap because of the order they were appended in the SVG. As you know, the SVG order defines what element goes over its siblings. So, when you expand the hovered arc (using the new startAngle and endAngle), the arc expands under a sibling that sits on top of it in the SVG order.
One solution is sorting the elements inside the mouseenter function, in such a way that the hovered element is the first one in the SVG order. This is the function:
svg.selectAll("path").sort(function (a, b) {
if (a != d) return -1;
else return 1;
});
The element that you hovered is the d, and a is the first one. Using this function, all the paths are sorted when you hover over them.
This is the Bin: http://jsbin.com/hotifepiko/1/edit?html,output
PS: This other Bin solves the problem of the disappearing texts (because of the sort function). I just created new groups for the texts: http://jsbin.com/mifejasiyo/1/edit?html,output
I've followed Mike Bostock's tutorial for rendering svg elements with D3 on top of a Leaflet map, but in addition to the functionality in his example I'd like for users to be able to reposition D3-rendered elements on the map by dragging them. That is, I want users to be able to geographically reposition objects in a D3-rendered SVG element in the leaflet-overlay-pane by dragging those elements, and have the updated geographic placements preserved when the user drags or zooms the Leaflet map.
With some very hacky html editing in the D3 drag events I've managed to do this (see jsFiddle - drag the blue circles to confirm functionality) but I'm hoping to find a better method. Any suggestions? Here is what I have so far:
var svg, g, map, collection, transform, path, json, d_string;
json = get_json();
setup_map();
function setup_map(){
map = L.map('map').fitBounds([[-3.82,-73.24],[-3.69,-73.35]]);
L.tileLayer('http://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png',{
maxZoom: 18,
attribution: 'OpenStreetMap, D3'
}).addTo(map);
map.on("viewreset", reset);
setup_d3();
}
function setup_d3(){
svg = d3.select(map.getPanes().overlayPane).append("svg");
g = svg.append("g").attr("class", "leaflet-zoom-hide");
drag = d3.behavior.drag()
.origin(function(d) { return d; })
.on("dragstart", dragstarted)
.on("drag", dragged)
.on("dragend", dragended);
transform = d3.geo.transform({point: projectPoint});
path = d3.geo.path().projection(transform);
g.selectAll("path")
.data(json.features)
.enter().append("path");
g.selectAll("path")
.attr('fill','blue')
.attr("r", 10)
.style("cursor", "pointer")
.call(drag);
reset();
}
function reset() {
var buffer_space = 200; //so point markers are fully drawn, and to give you space to move markers around
bounds = path.bounds(json);
var topLeft = bounds[0], bottomRight = bounds[1];
topLeft[0] -= buffer_space;
topLeft[1] -= buffer_space;
bottomRight[0] += buffer_space;
bottomRight[1] += buffer_space;
svg.attr("width", bottomRight[0] - topLeft[0])
.attr("height", bottomRight[1] - topLeft[1])
.style("left", topLeft[0] + "px")
.style("top", topLeft[1] + "px");
g.attr("transform", "translate(" + -topLeft[0] + "," + -topLeft[1] + ")");
g.selectAll("path").attr("d", path);
}
function projectPoint(x, y) {
var point = map.latLngToLayerPoint(new L.LatLng(y, x));
this.stream.point(point.x, point.y);
}
function dragstarted(d) {
d3.event.sourceEvent.stopPropagation();
d3.select(this).classed("dragging", true);
d_string = d3.select(this).attr("d");
d_string = d_string.substring(d_string.indexOf("m"));
}
function dragged(d) {
var offset = get_leaflet_offset();
var size = d3.select(this).attr("r")/2;
var pt = [d3.event.sourceEvent.clientX - size - offset[0], d3.event.sourceEvent.clientY - size - offset[1]];
var hackpath = "M" + pt[0] + "," + pt[1] + d_string;
d3.select(this).attr("d", hackpath);
}
function dragended(d) {
var offset = get_leaflet_offset();
var size = d3.select(this).attr("r")/2;
var pt = layer_to_LL(d3.event.sourceEvent.clientX - size - offset[0], d3.event.sourceEvent.clientY - size - offset[1]);
d.geometry.coordinates = [pt.lng, pt.lat];
d3.select(this).classed("dragging", false);
reset();
}
function get_leaflet_offset(){
var trfm = $(".leaflet-map-pane").css('transform');
trfm = trfm.split(", ");
return [parseInt(trfm[4]), parseInt(trfm[5])];
}
function layer_to_LL(x,y){return map.layerPointToLatLng(new L.Point(x,y));}
function projectPoint(x, y) {
var point = map.latLngToLayerPoint(new L.LatLng(y, x));
this.stream.point(point.x, point.y);
}
function projectSinglePoint(x, y) {
var point = map.latLngToLayerPoint(new L.LatLng(y, x));
console.log(point);
return point;
}
function get_json(){
return {
"type": "FeatureCollection",
"crs": {
"type": "name",
"properties": {"name": "urn:ogc:def:crs:EPSG::4269"}
},
"features": [{
"type": "Feature",
"properties": {"id": "pt0"},
"geometry": {
"type": "Point",
"coordinates": [-73.25, -3.72]
}
}, {
"type": "Feature",
"properties": {"id": "pt1"},
"geometry": {
"type": "Point",
"coordinates": [-73.37, -3.82]
}
}, {
"type": "Feature",
"properties": {"id": "pt2"},
"geometry": {
"type": "Point",
"coordinates": [-73.32, -3.67]
}
}]
}
}
Thanks,
Chris
In my opinion I would have used the default leaflet option to make markers and set it as draggable
Something like this:
json.features.forEach(function(d) {
var marker = L.marker(new L.LatLng(d.geometry.coordinates[1], d.geometry.coordinates[0]), {
draggable: true
});
marker.addTo(map);
})
This will relieve you of doing the dragging logic on D3 you can see how much code got reduced :)
Working code here
Marker documentation here
Hope this helps!
I want to show on the chart D3 e-mail communications between users using data from JSON file on chart FLARE or other.
Users can be represented on a graph as a node and e-mails between them as links.
If it is possible to present in the D3 and someone knows the solution to this problem please let me know.
The following sample array of data for a single email.
In the other tables changes to email details: user names, titles, emails, dates and times.
{
"metadataAsStrings": {
"doc-from": "User 1",
"doc-sender": "User 1",
"caat-derived-recipients": "User 2",
"doc-subject": "Title Email 1"
"doc-recipient": "User 2",
"caat-normalized-author": "User 1",
"caat-derived-email-action": "REPLY"
"caat-derived-end-email": "true",
"caat-derived-inclusive-email-reason": "MESSAGE"
"doc-date": "2014/09/25 10:20:00",
"doc-is", "User 2"
}
}
This is pretty easy with the force layout.
Here's the plnkr:
http://plnkr.co/edit/1Mub7rTUKQuuAB6TAoJb?p=preview
What I actually did was create a json according to the structure that d3 force layout needs.
Assuming I have something similar to your data, I do a little bit of parsing:
{
"edges": [
{
"source":"1",
"target": "2",
"color": "yellow",
"weight": "1.0",
"doc-subject": "Title Email 1"
},
{
"source":"2",
"target": "3",
"color": "blue",
"weight": "1.0",
"doc-subject": "Title Email 2"
}
],
"nodes": [
{
"label":"user 1",
"x":-1015.1223754882812,"y":679.421875,
"id":"1","attributes":{},"color":"rgb(175,156,171)",
"size":20
},
{
"label":"user 2",
"x":-915.1223754882812,"y":659.421875,
"id":"2","attributes":{},"color":"rgb(175,156,171)",
"size":15
},
{
"label":"user 3",
"x":-1015.1223754882812,"y":579.421875,
"id":"3","attributes":{},"color":"rgb(175,156,171)",
"size":15
}
]
}
Then, in d3, I have this code to parse it:
d3.json("graph.json", function(error, graphData) {
//setup the data
var graph = {};
graph.nodes = [];
graph.links = [];
var test = [];
// set the node data
for (var nodeIndex in graphData.nodes){
var curr_node = graphData.nodes[nodeIndex];
graph.nodes[curr_node.id] = {
x: curr_node.x,
y: curr_node.y,
color: curr_node.color,
size: curr_node.size,
label: curr_node.label,
id: curr_node.id
};
test.push(Number(curr_node.id));
}
// sort the IDs
function sortNumber(a,b) {
return a - b;
}
test.sort(sortNumber);
// now go over each ID and set it in the
var tmpNodes = [];
for (var index in test){
tmpNodes.push(graph.nodes[test[index]]);
}
graph.nodes = tmpNodes;
// now setup the edges/links
for (edge in graphData.edges){
var curr_link = graphData.edges[edge];
graph.links.push({source: test.indexOf(Number(curr_link.source)), target: test.indexOf(Number(curr_link.target)), weight: 1.0});
}
force
.nodes(graph.nodes)
.links(graph.links)
.start();
link = link.data(graph.links)
.enter().append("line")
.attr("class", "link");
node = node
.data(graph.nodes)
.enter().append("g")
.attr("class", "node")
.call(drag)
.on("dblclick", dblclick)
.on("mouseover", function(d){
hover.html(d.id + ": " + d.label);
})
.on("mouseleave", function(d){
hover.html("");
})
;
node.append("circle")
.attr("r", function(d,i){
return d.size/2;
})
.attr("fill", function(d,i){
return d.color;
})
;
var textNode = node.append("g");
var text = textNode.append("text")
.attr("dx", 12)
.attr("dy", ".35em")
.attr("font-size", function(d){
return 12+(d.size-1)/7+"px";
})
.text(function(d) {
return d.label });
textNode.append("rect")
.attr("x",function(d,i){
var g = node[0][i].childNodes[1];
return -g.getBBox().width;
})
.attr("y",function(d,i){
var g = node[0][i].childNodes[1].childNodes[0];
return -g.getBBox().height+10;
})
.attr("fill","white")
.attr("fill-opacity",0.25)
.attr("width",function(d,i){
var g = node[0][i].childNodes[1];
return g.getBBox().width;
})
.attr("height",function(d,i){
var g = node[0][i].childNodes[1].childNodes[0];
return g.getBBox().height;
})
;
});
So what happens in the code, d3 gets the json, I do a little bit of parsing for d3 to get the data setup better (I made it like that so I could add more users, and sort them by their ID rather then the order in which they are set in the array) and then just give the links and nodes to d3 to plot.
Hope this helps.