svg - Aligning text outside circular arc d3js - d3.js

Can someone help me to create below image using d3js. I able to create pie chart as required but stuck to render outer text with arrows and all.
Wheel with outer text
As of know I have achieved circle creation using below code.
var svg = d3.select("svg");
var margin = {top: 40, right: 45, bottom: 30, left: 40};
console.log(svg);
var width = svg.attr('width');
var height = svg.attr('height');
var radius = Math.min(width, height)/2;
var g = svg.append("g").attr("transform", "translate(" + width / 2 + "," + height / 2 + ")");
var hoverStyle = {
zindex: '2px'
};
var hoverExitStyle = {
zindex: "0px"
}
var animateSpeed = 500;
// Define a Pie
var pie = d3.pie()
.sort(null)
.value(function(d) {return d.number});
// define pie section
var path = d3.arc()
.outerRadius(radius - 10)
.innerRadius(0);
//
var label = d3.arc()
.outerRadius(radius - 40)
.innerRadius(radius - 40);
// Get pie sections based on the data.
var pieSections = pie(data);
var arc = g.selectAll('.arc')
.data(pieSections)
.enter().append("g")
.attr("class", "arc")
.append('a')
.attr("href", function(d) { return d.data.url; });
arc.append("path")
.attr("d", path).transition()
.attr("fill", function(d) { return d.data.color; });
arc.append("text")
.attr("transform", function(d) { return "translate(" + label.centroid(d) + ")"; })
.attr("dy", "0.35em")
.text(function(d) { return d.data.title; });

The text and the arrows are two separate concerns that probably merit their own questions.
Curved text
To do text on a path with d3, you might want to look at the textpath documentation. It's going to be a little tricky; basically, you'll want to create a second d3.arc() generator with a slightly longer outer radius. Use the longer one to set the d attribute of path elements (that you need to create) inside the SVG's defs object, and reference those path elements' ids from textpath elements (that you also need to create).
Curved arrows
To accomplish this exactly like the image, you're probably going to need to some manual construction (including figuring out the math!) of the d attribute yourself to add appropriate arrowheads (see the SVG path syntax). If you're doing a static image, it might be faster to just create the lines (again, using a longer-radius d3.arc() generator), and export the SVG with something like SVG crowbar to a drawing program like Illustrator or Inkscape, and add the arrowheads there.

Related

How do I use event listeners to swtich my d3 bar chart where I only see one and not both?

I have 2 buttons that i want to use to control what data set I am using for my bar chart. Right now I can click on one and it shows my d3 graph without problems. But when I want to switch to the other graph, I click on the button and it shows me that graph on top of my previous graph. How do I make it so that when I switch between graphs, it only shows me one graph.
var djockey = 'top5jockey.csv'
var dtrainer = 'top5trainer.csv'
// Define SVG area dimensions
var svgWidth = 1500;
var svgHeight = 1000;
// Define the chart's margins as an object
var chartMargin = {
top: 30,
right: 30,
bottom: 130,
left: 30
};
// Define dimensions of the chart area
var chartWidth = svgWidth - chartMargin.left - chartMargin.right;
var chartHeight = svgHeight - chartMargin.top - chartMargin.bottom;
// Select body, append SVG area to it, and set the dimensions
var svg = d3.select("body")
.append("svg")
.attr("height", svgHeight)
.attr("width", svgWidth);
// Append a group to the SVG area and shift ('translate') it to the right and to the bottom
var chartGroup = svg.append("g")
.attr("transform", `translate(${chartMargin.left}, ${chartMargin.top})`);
var btnj = document.getElementById("Jockey")
btnj.addEventListener('click', function(e){
change(e.target.id)
})
var btnt = document.getElementById("Trainer")
btnt.addEventListener('click', function(e){
change(e.target.id)
})
function change(value){
if(value === 'Jockey'){
update(djockey);
}else if(value === 'Trainer'){
update(dtrainer);
}
}
function update(data){
d3.csv(data).then(function(data) {
console.log(data);
// Cast the hours value to a number for each piece of tvData
data.forEach(function(d) {
d.Count = +d.Count;
});
// Configure a band scale for the horizontal axis with a padding of 0.1 (10%)
var xScale = d3.scaleBand()
.domain(data.map(d => d.Name))
.range([0, chartWidth])
.padding(0.1);
// Create a linear scale for the vertical axis.
var yLinearScale = d3.scaleLinear()
.domain([0, d3.max(data, d => d.Count)])
.range([chartHeight, 0]);
// Create two new functions passing our scales in as arguments
// These will be used to create the chart's axes
var bottomAxis = d3.axisBottom(xScale);
var leftAxis = d3.axisLeft(yLinearScale).ticks(10);
// Append two SVG group elements to the chartGroup area,
// and create the bottom and left axes inside of them
chartGroup.append("g")
.call(leftAxis);
chartGroup.append("g")
.attr("class", "x_axis")
.attr("transform", `translate(0, ${chartHeight})`)
.call(bottomAxis)
.selectAll("text")
.style("text-anchor", "end")
.attr("dx", "-.8em")
.attr("dy", ".15em")
.attr("transform", "rotate(-65)");
// Create one SVG rectangle per piece of tvData
// Use the linear and band scales to position each rectangle within the chart
chartGroup.selectAll("#bar")
.data(data)
.enter()
.append("rect")
.attr("class", "bar")
.attr("x", d => xScale(d.Name))
.attr("y", d => yLinearScale(d.Count))
.attr("width", xScale.bandwidth())
.attr("height", d => chartHeight - yLinearScale(d.Count));
}).catch(function(error) {
console.log(error);
})
};
D3 has a function allowing you to remove all svg elements. Basically, you select the svg, then run .remove() at the top of your event listener. It will clear out all svg elements.

Rotating the text in the pie chart of d3.js and alternate filling of colors

I have this d3 code for drawing the pie chart in d3.js
/** START OF PIE CHART */
var svgCirWidth = 600, svgCirHeight = 300, radius = Math.min(svgCirWidth, svgCirHeight) / 2;
const pieContainer = d3.select("#pieChart")
.append("svg")
.attr("width", svgCirWidth)
.attr("height", svgCirHeight);
//create group element to hold pie chart
var g = pieContainer.append("g")
.attr("transform", "translate(" + 250 + "," + radius + ")");
var color = d3.scaleOrdinal(d3.schemeCategory10);
var pie = d3.pie().value(function (d) {
return d.total_up_percentage;
});
var path = d3.arc()
.outerRadius(radius)
.innerRadius(0);
var arc = g.selectAll("arc")
.data(pie(data))
.enter() //means keeps looping in the data
.append("g");
arc.append("path")
.attr("d", path)
.attr("fill", function (d) {
return color(d.data.total_up_percentage);
})
.append("text")
.text("afdaf");
var label = d3.arc()
.outerRadius(radius)
.innerRadius(0);
arc.append("text")
.attr("transform", (d) => {
return "translate(" + label.centroid(d) + ")";
})
.attr("text-anchor", "middle")
.text((d) => {
return d.data.region_iso_code + ":" + d.data.total_up_percentage + "%"
});
and this is the result of my pie
as you can see the text overlaps each other. I was wondering how can i rotate the text so it can be much more easier to read. I've tried editing the transform in the console but it won't work it just makes the text go up or down. Also I was wondering what happened to the color of my pie. It stuck on orange. It says on the documentation i read about this schemeCategory10 is that it is a 10 color code scheme. Yet it won't show the rest of the color. Is there any other way to change color?
When using an ordinal scale you should never rely on the scale's ability to infer the domain from usage: a good practice is always to explicitly set the domain.
By setting the domain you'd quickly see that this is indeed the expected behaviour: all orange slices have the same value, which is 100.
If you want different colors for those same values, use the indices instead:
.attr("fill", function (_, i) {
return color(i);
})
PS: regarding the texts, please avoid asking 2 or more different issues in a single question. Edit your question leaving just 1 issue, you can always post a new question with the other issues.

D3 V4 Properly placing a bubble in the US Map

I am creating a US Map and I have a series of ACTUAL coordinates of some places in US. I would like to put a point or bubble on the right spot in the map. How do I scale/translate these?
This is what I get:
With what I have tried:
function USAPlot(divid, data) {
var margin = { top: 20, right: 20, bottom: 30, left: 50 },
width = 1040 - margin.left - margin.right,
height = 700 - margin.top - margin.bottom;
// formatting the data
data.forEach(function (d) {
d.loc = d.location;
d.count = d.count;
d.lat = d.latitude;
d.lon = d.longitude;
});
var svg = d3.select(divid)
.append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
;
var path = d3.geoPath();
var projection = d3.geoMercator()
.scale(200)
.translate([margin.left + width / 2, margin.top + height / 2])
d3.json("https://d3js.org/us-10m.v1.json", function (error, us) {
if (error) throw error;
svg.append("g")
.attr("class", "states")
.attr("fill-opacity", 0.4)
.selectAll("path")
.data(topojson.feature(us, us.objects.states).features)
.enter().append("path")
.attr("d", path);
svg.append("path")
.attr("class", "state-borders")
.attr("d", path(topojson.mesh(us, us.objects.states, function (a, b) { return a !== b; })));
});
svg.selectAll("myCircles")
.data(data)
.enter()
.append("circle")
.attr("cx", function (d) { return projection([d.lon, d.lat])[0]; })
.attr("cy", function (d) { return projection([d.lon, d.lat])[1]; })
.attr("r", 14) //first testing with fixed radius and then will scale acccording to count
.style("fill", "69b3a2")
.attr("stroke", "#69b3a2")
.attr("stroke-width", 3)
.attr("fill-opacity", 1);
}
I have no idea whether these bubbles are dropping at the actual place - which I am definitely looking for.
As far as a testing method to see if features are alinging properly, try placing easy to identify landmarks, I use Seatle and Miami below - they're on opposite sides of the area of interest, and it should be easy to tell if they are in the wrong place (in the water or inland).
I'm not sure where they are supposed to fall as I do not have the coordinates but I can tell you they aren't where they are supposed to be.
The reason I can know this is because you are using two different projections for your data.
Mercator Projection
You define one of the projections and use it to position the dots:
var projection = d3.geoMercator()
.scale(200)
.translate([margin.left + width / 2, margin.top + height / 2])
This is a Mercator projection centred at [0°,0°] (by default). Here is the world projected with that projection (with the margin and same sized SVG):
D3 GᴇᴏMᴇʀᴄᴀᴛᴏʀ Wɪᴛʜ Cᴇɴᴛᴇʀ [0,0] ᴀɴᴅ Sᴄᴀʟᴇ 200
You are projecting coordinates for the circles based on this projection.
For reproducability, here's a snippet - you should view in full screen:
var margin = { top: 20, right: 20, bottom: 30, left: 50 },
width = 1040 - margin.left - margin.right,
height = 700 - margin.top - margin.bottom;
d3.json("https://cdn.jsdelivr.net/npm/world-atlas#2/land-50m.json").then(function(json) {
var svg = d3.select("body")
.append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
var projection = d3.geoMercator()
.scale(200)
.translate([margin.left + width / 2, margin.top + height / 2])
var path = d3.geoPath().projection(projection);
svg.append("g")
.attr("class", "states")
.attr("fill-opacity", 0.4)
.selectAll("path")
.data(topojson.feature(json, json.objects.land).features)
.enter().append("path")
.attr("d", path);
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/topojson/3.0.2/topojson.js"></script>
A mystery projection
The second projection is not obvious. If you look at the snippet used to create the above image, you'll notice that it assigns the projection to the path:
var path = d3.geoPath().projection(projection);
This is so the path converts each geographic coordinate (a spherical latitude/longitude pair) to the correct coordinate on the screen (a Cartesian pixel x,y value): a coordinate of [-17°,85°] will be converted to something like [100px,50px].
In your question you simply use:
var path = d3.geoPath();
You don't assign a projection to the path - so d3.geoPath() simply plots every vertice/point in the geojson/topojson as though the coordinate contains pixel coordinates: a coordinate of [100px,50px] in the geojson/topojson is plotted on the SVG at x=100, y=50.
Despite not using a projection, your the US states plot as expected. Why? Because the geojson/topojson was already projected. Since it was preprojected, we don't need to use a projection when we plot it with D3.
Pre-projected geometry can be useful as it requires less calculations to draw, resulting in faster rendering speeds, but comes at a cost of less flexibility (see here).
If we overlay your pre-projected geometry with the geometry you project with d3.geoProjection, we get:
Naturally, you can see there is no point that is the same between the two. Consequently, you are not projecting points so that they properly overlay the pre-projected geometries.
Cᴏᴍᴘᴀʀɪsᴏɴ ʙᴇᴛᴡᴇᴇɴ ᴛʜᴇ ᴛᴡᴏ ᴘʀᴏᴊᴇᴄᴛɪᴏɴs
Snippet to reproduce:
var margin = { top: 20, right: 20, bottom: 30, left: 50 },
width = 1040 - margin.left - margin.right,
height = 700 - margin.top - margin.bottom;
var svg = d3.select("body")
.append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
d3.json("https://cdn.jsdelivr.net/npm/world-atlas#2/land-50m.json").then(function(json) {
var projection = d3.geoMercator()
.scale(200)
.translate([margin.left + width / 2, margin.top + height / 2])
var path = d3.geoPath().projection(projection);
svg.append("g")
.attr("fill-opacity", 0.4)
.selectAll("path")
.data(topojson.feature(json, json.objects.land).features)
.enter().append("path")
.attr("d", path);
})
d3.json("https://d3js.org/us-10m.v1.json").then(function(us) {
var path = d3.geoPath();
svg.append("g")
.attr("fill-opacity", 0.4)
.selectAll("path")
.data(topojson.feature(us, us.objects.states).features)
.enter().append("path")
.attr("d", path);
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/topojson/3.0.2/topojson.js"></script>
Unsatisfactory Solution
Without metadata explaining what projection and coordinate system a geojson/topojson uses, we generally cannot duplicate that projection to overlay other features.
In this case, however, if we look carefully at the plotted US states, we can see that an Albers projection was used to pre-project the state outlines.
Sometimes, we can guess the projection parameters. As I'm fairly familiar with this file (), I can tell you it uses the following parameters:
d3.geoAlbersUsa()
.scale(d3.geoAlbersUsa().scale()*6/5)
.translate([480,300]);
Here's an example showing Miami and Seattle overlain:
var width = 960,height = 600;
var svg = d3.select("body")
.append("svg")
.attr("width",width)
.attr("height",height);
d3.json("https://d3js.org/us-10m.v1.json").then(function(us) {
var path = d3.geoPath();
var projection = d3.geoAlbersUsa()
.scale(d3.geoAlbersUsa().scale()*6/5)
.translate([width/2,height/2]);
svg.append("g")
.attr("fill-opacity", 0.4)
.selectAll("path")
.data(topojson.feature(us, us.objects.states).features)
.enter().append("path")
.attr("d", path);
var places = [
[-122.3367534,47.5996582],
[-80.1942949,25.7645783]
]
svg.selectAll(null)
.data(places)
.enter()
.append("circle")
.attr("r", 3)
.attr("transform", function(d) {
return "translate("+projection(d)+")";
})
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/topojson/3.0.2/topojson.js"></script>
But, this has a downside of being very obtuse in adopting for other screen sizes, translations, centers, scales, etc. Pre-projected geometry also generates a lot of confusion when combined with unprojected geometry. For example, this question shows a common frustration on sizing and centring pre-projected geometry properly.
Better Solution
A better solution is to use one projection for everything. Either pre-project everything first (which is a bit more complex), or project everything on the fly (it really doesn't take that long for a browser). This is just clearer and easier when modifying the visualization or the geographic data.
To project everything the same way, you'll need to make sure all your data is unprojected, that is to say it uses lat/long pairs for its coordinates / coordinate space. As your US json is pre-projected, we'll need to find another, perhaps:
https://bl.ocks.org/mbostock/raw/4090846/us.json
And we simply run everything through the projection:
Snippet won't load the resource, but here's a bl.ock, with the code shown below:
var width =960,height = 600;
var svg = d3.select("body")
.append("svg")
.attr("width",width)
.attr("height",height);
d3.json("us.json").then(function(us) {
var projection = d3.geoAlbersUsa()
.scale(150)
.translate([width/2,height/2]);
var path = d3.geoPath().projection(projection);
svg.append("g")
.attr("fill-opacity", 0.4)
.selectAll("path")
.data(topojson.feature(us, us.objects.states).features)
.enter().append("path")
.attr("d", path);
var places = [
[-122.3367534,47.5996582],
[-80.1942949,25.7645783]
]
svg.selectAll(null)
.data(places)
.enter()
.append("circle")
.attr("r", 3)
.attr("transform", function(d) {
return "translate("+projection(d)+")";
})
})

D3: How to refresh a chart with new data?

I have created a d3 donut chart. Here is my code:
var width = 480;
var height = 480;
var radius = Math.min(width, height) / 2;
var doughnutWidth = 30;
var color = d3.scale.category10();
var arc = d3.svg.arc()
.outerRadius(radius - 10)
.innerRadius(radius - 70);
var pie = d3.layout.pie()
.sort(null)
.value(function(d) { return d[1]; });
var dataset = settings.dataset;
console.log(dataset);
var svg = d3.select("body")
.append("svg")
.attr("width", width)
.attr("height", height)
.append("g")
.attr("transform", "translate(" + width / 2 + "," + height / 2 + ")");
var path = svg.selectAll('path')
.data(pie(dataset))
.enter()
.append('path')
.attr('d', arc)
.attr('fill', function(d, i) {
return color(d.data[0]);
})
I have a simple form on my web page which displays a dropdown menu with several options. Every time a user changes a value on the form a new dataset is sent to my script (settings.dataset) and the donut is redrawn on the page.
Problem is, some of the values from the previous dataset remain in the DOM. In the console output below, you can see that the second dataset only has two elements. The third one is from the previous dataset. This is messing up the chart, as it is displaying a value that shouldn't be there.
My question: how do I clear the old values? I've read up on .exit() and .remove(), but I can't get my head around these methods.
Create one function that (re)draws the pie when it's created and when it's updated.
New data should be added to pie using enter() and old data should be removed using exit().remove()
It is as simple as this:
path.enter().append("path")
.attr("fill", function(d, i) { return color(i); })
.attr("d", arc)
.each(function(d) {this._current = d;} );
path.transition()
.attrTween("d", arcTween);
path.exit().remove()
Full working code -> JSFIDDLE
There are two steps to implement the 'redraw' effect you want:
First, I suppose you want the svg canvas to be drawn only once when the page is loaded for the first time, and then update the chart in the svg instead of remove and redraw the svg:
var svg = d3.select("body")
.selectAll("svg")
.data([settings.dataset]);
// put data in an array with only one element
// this ensures there is only one consistent svg on the page when data is updated(when the script gets executed)
svg.enter().append("svg")
Second, understanding enter(), exit(), there are many great tutorials about this. In your case, I would suggest to draw the donut something like this:
var path = svg.selectAll(".donut")
.data(settings.data)
// bind data to elements, now you have elements belong to one of those
// three 'states', enter, exit, or update
// for `enter` selection, append elements
path.enter().append("path").attr("d", arc).attr("fill", "teal")
// for `update` selection, which means they are already binded with data
path.transition().attrTween("d", someFunction) // apply transition here if you want some animation for data change
// for `exit` selection, they shouldn't be on the svg canvas since there is no corresponding data, you can then remove them
path.exit().remove()
//remove and create svg
d3.select("svg").remove();
var svg = d3.select("body").append("svg").attr("width","960").attr("height", "600"),
inner = svg.append("g");

meteor add d3 grafics dynamically fails

I'm drawing a d3 donut. Now I want to add as many donuts as entries in database. If I add something to database, automatic updating fails. I have to reload My code in the Browser - then I see the new donut. Isnt Meteor.autorun updating automatically?
Code is:
Template.donuts.rendered = function (){
var self = this;
self.node = self.find("p");
// Data
var dataset = {
apples: [2, 2, 2, 2, 2]
};
//Width and height
var width = 100,
height = 100,
radius = Math.min(width, height) / 2;
// render
self.handle = Meteor.autorun(function () {
var color = d3.scale.category10();
var pie = d3.layout.pie()
.sort(null);
var arc = d3.svg.arc()
.innerRadius(radius - 20)
.outerRadius(radius - 5);
var svg = d3.select(self.node).append("svg")
.attr("width", width)
.attr("height", height)
.append("g")
.attr("transform", "translate(" + width / 2 + "," + height / 2 + ")");
var path = svg.selectAll("path")
.data(pie(dataset.apples))
.enter().append("path")
.attr("fill", function(d, i) { return color(i); })
.attr("d", arc);
});
}; //Template.donuts
it is called via handlebars
<template name="donuts">
{{#each nodes}}
<p></p>
{{/each}}
</template>
What am I doing wrong. Thank you for your time.
Your rendered hook is on the wrong level. Right now you're connecting it to the template that contains the donuts, when it looks like you want to have each donut be rendered in a certain way. First, start by reorganising your templates:
<template name="donuts">
{{#each nodes}}
{{> node}}
{{/each}}
</template>
<template name="node"><p></p></template>
Now you can tell a node what to do when it's rendered:
Template.node.rendered = function() {
// d3 code
}
The rendered call will be automatically run whenever the node is re-rendered, which will happen if you change a dependency. If nodes is a reactive source like a mongodb cursor, this will work immediately. Otherwise, please add more code so we can figure out what else is going on.
Meteor.autorun() will run whenever its dependencies change. You need a reactive datasource inside the function.
Found a more elegant solution:
// Donuts //
function donutinit() {
var dataset = {
apples: [2, 2, 2, 2, 2]
};
//Width and height
var width = 100,
height = 100,
radius = Math.min(width, height) / 2;
// render
var color = d3.scale.category10();
var pie = d3.layout.pie()
.sort(null);
var arc = d3.svg.arc()
.innerRadius(radius - 20)
.outerRadius(radius - 5);
var svg = d3.select("#donut_canvas").append("svg")
.attr("width", width)
.attr("height", height)
.append("g")
.attr("transform", "translate(" + width / 2 + "," + height / 2 + ")");
var path = svg.selectAll("path")
.data(pie(dataset.apples))
.enter().append("path")
.attr("fill", function(d, i) { return color(i); })
.attr("d", arc);
};
Template.donut.rendered = function() {
donutinit();
};
After that iterate with handlebars over #donut_canvas. The Meteor.autorun or Meteor.rendered gave me unpredictable amounts of donuts - it rendered additional donuts. I had to reload it then.
Answer is inspired from here: Google map contained in meteor Template is rendered twice
Thank you for your time.

Resources