Given a pie chart with an arc, I'd like to append lines at each arcs center points to follow the path. I'm able to append the lines on the path properly currently but I'm not able to angle them appropriately. I have a set of circles going around my pie chart and need to place these lines slightly below the circles at the mid point between 1 circle and the next. Here is a pretty gross image of what I'm trying to do, the lines should be at the center of the arc but just below the circles and following the path of the arc.
And here is what I am currently getting with my code.I'm trying to get the dark black lines to follow a similar rotation to the light gray ones that you see at the center.
const startAngle = (-45 * Math.PI) / 180;
const endAngle = (-45 * Math.PI) / 180 + 2 * Math.PI;
const width = 500;
const height = Math.min(width, 500);
const viewBox = [-width / 2, -height / 2, width, height];
const svg = d3
.select("#canvas")
.append("svg")
.attr("viewBox", viewBox);
function drawGridDonut(svg, innerFactor, outerFactor) {
const radius = Math.min(width, height) / 2;
const graph = new Array(6); // 6 equal sections
const gridPie = d3
.pie()
.startAngle(startAngle)
.endAngle(endAngle)
.sort(null)
.value(1);
const arc = d3
.arc()
.innerRadius(radius * innerFactor)
.outerRadius(radius * outerFactor);
// base path + arc
svg
.append("g")
.attr("class", "grid")
.selectAll("path")
.data(gridPie(graph))
.join("path")
.attr("stroke-width", 1)
.attr("fill", "none")
.attr("d", arc);
// border lines
svg
.selectAll(".grid")
// border lines, 1 = dark border, 0 = light border
.data(gridPie([1, 0, 1, 0, 1, 0]))
.append("line")
.attr("x1", 0)
.attr("y1", 0)
.attr(
"y2",
d =>
Math.sin(d.startAngle - Math.PI / 2) *
(radius - (d.data === 1 ? 0 : 75.5))
)
.attr(
"x2",
d =>
Math.cos(d.startAngle - Math.PI / 2) *
(radius - (d.data === 1 ? 0 : 75.5))
)
.attr("stroke", d => (d.data === 0 ? "#C8C8C8" : "#000"))
.attr("stroke-width", "0.1");
// dots for overuse + benefits
svg
.selectAll(".dot")
.data(gridPie([...Array(12)]))
.enter()
.append("circle")
.attr('class', '.dot')
.attr("cx", d => arc.centroid(d)[0])
.attr("cy", d => arc.centroid(d)[1])
.attr("r", 1)
.style("fill", "#C8C8C8");
// this is where the lines are being made
svg
.selectAll('.hash')
.data(gridPie([...Array(12)]))
.enter()
.append('line')
.attr('class', '.hash')
.attr('d', arc)
.attr("x1", (d) => arc.centroid(d)[0] - d.startAngle)
.attr("y1", (d) => arc.centroid(d)[1] - d.startAngle)
.attr('x2', d => arc.centroid(d)[0] + d.endAngle )
.attr('y2', d => arc.centroid(d)[1] + d.endAngle)
.attr('width', 10)
.attr('height', 4)
.attr("stroke", "black")
.attr('transform', (d) => 'rotate(180)')
}
for (let i = 0; i < 6; i++) {
drawGridDonut(svg, 0.9 - 0.04, 0.9 + 0.002)
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<div id="canvas"></div>
I fixed this by drawing a line from the center of my pie to the top then adding dasharray (with fine tuning of the inner and outer arcs to mark the safe spaces.
function drawHyphen(svg, innerFactor, outerFactor, dashPercent) {
const radius = Math.min(width, height) / 2;
const gridPie = d3
.pie()
.startAngle(startAngle)
.endAngle(endAngle)
.sort(null)
.value(1);
svg
.selectAll('.line')
.data(gridPie([...Array(12)]))
.enter()
.append("line")
.attr('class', (d, i) => `line${i}`)
.attr(
"y1",
d =>
Math.sin((d.startAngle) - Math.PI / 4) * radius * innerFactor
)
.attr(
"x1",
d =>
Math.cos((d.startAngle) - Math.PI / 4) * radius * innerFactor
)
.attr(
"y2",
d =>
Math.sin((d.startAngle) - Math.PI / 4) * radius / outerFactor
)
.attr(
"x2",
d =>
Math.cos((d.startAngle) - Math.PI / 4) * radius / outerFactor
)
.attr("stroke", "#EEEEEE")
.attr("stroke-dasharray", `0.5, ${dashPercent}`)
.attr("stroke-width", 4)
}
then I call the function as many times as I need to and set the outerFactor to be the threshold where the safe space will be.
let outerArc = 1 - 0.051
drawHyphen(svg, outerArc - 0.835, outerArc - 2.3, "1.9%")
Related
I am working on a project with d3.js where I want a visualisation to centre on the screen. My current solution works perfectly on desktop screens, even when resizing, but when I load the webpage on my iPhone screen, the visualisation is no longer centred. The target div for this visualisation is:
<div id="grades_circular" class="my_dataviz"></div>
The css to style is:
.my_dataviz {
display: flex;
justify-content: center;}
The JavaScript code for the visualisation is :
const grades_margin = {top: 30, right: 0, bottom: 70, left: 0},
grades_width = 460 - grades_margin.left - grades_margin.right,
grades_height = 460 - grades_margin.top - grades_margin.bottom;
innerRadius = 50,
outerRadius = Math.min(grades_width, grades_height) / 2;
const grades_svg = d3.select("#grades_circular")
.append("svg")
.attr("width", grades_width + grades_margin.left + grades_margin.right)
.attr("height", grades_height + grades_margin.top + grades_margin.bottom)
.append("g")
.attr("transform", `translate(${grades_width/2+grades_margin.left}, ${grades_height/2+grades_margin.top})`);
d3.csv("https://raw.githubusercontent.com/ben-austin27/ben-austin27.github.io/main/data/results.csv").then( function(grades_data) {
const grades_x = d3.scaleBand()
.range([0, 2 * Math.PI]) // X axis goes from 0 to 2pi = all around the circle. If I stop at 1Pi, it will be around a half circle
.align(0) // This does nothing
.domain(grades_data.map(d => d.module)); // The domain of the X axis is the list of states.
const grades_y = d3.scaleRadial()
.range([innerRadius, outerRadius]) // Domain will be define later.
.domain([40, 100]); // Domain of Y is from 0 to the max seen in the data
// Add the bars
bars = grades_svg.append("g")
.selectAll("path")
.data(grades_data)
.join("path")
.attr("fill", d => "#" + d.color )
.attr("d", d3.arc() // imagine your doing a part of a donut plot
.innerRadius(innerRadius)
.outerRadius(innerRadius+0.05)//d => grades_y(d['grade'])
.startAngle(d => grades_x(d.module))
.endAngle(d => grades_x(d.module) + grades_x.bandwidth())
.padAngle(0.05)
.padRadius(innerRadius))
modules = grades_svg.append("g")
.selectAll("g")
.data(grades_data)
.join("g")
.attr("text-anchor", function(d) { return (grades_x(d.module) + grades_x.bandwidth() / 2 + Math.PI) % (2 * Math.PI) < Math.PI ? "end" : "start"; })
.attr("transform", function(d) { return "rotate(" + ((grades_x(d.module) + grades_x.bandwidth() / 2) * 180 / Math.PI - 90) + ")"+"translate(" + (innerRadius+10) + ",0)"; })//
.append("text")
.text(function(d){return(d.module)})
.attr("transform", function(d) { return (grades_x(d.module) + grades_x.bandwidth() / 2 + Math.PI) % (2 * Math.PI) < Math.PI ? "rotate(180)" : "rotate(0)"; })
.style("font-size", "11px")
.attr("alignment-baseline", "middle")
grades = grades_svg.append("g")
.selectAll("g")
.data(grades_data)
.join("g")
.attr("text-anchor", function(d) { return (grades_x(d.module) + grades_x.bandwidth() / 2 + Math.PI) % (2 * Math.PI) < Math.PI ? "end" : "start"; })
.attr("transform", function(d) { return "rotate(" + ((grades_x(d.module) + grades_x.bandwidth() / 2) * 180 / Math.PI - 90) + ")"+"translate(" + (grades_y(d['grade'])+7) + ",0)"; })//
.append("text")
.text(function(d){return(d.grade)})
.attr("transform", function(d) { return (grades_x(d.module) + grades_x.bandwidth() / 2 + Math.PI) % (2 * Math.PI) < Math.PI ? "rotate(180)" : "rotate(0)"; })
.style("font-size", "11px")
.attr("alignment-baseline", "middle")
function update_bars() {
d3.selectAll("path")
.transition()
.ease(d3.easePolyInOut.exponent(3)) //https://observablehq.com/#d3/easing-animations
.duration(2000)
.attr("d", d3.arc() // imagine your doing a part of a donut plot
.innerRadius(innerRadius)
.outerRadius(d => grades_y(d['grade']))
.startAngle(d => grades_x(d.module))
.endAngle(d => grades_x(d.module) + grades_x.bandwidth())
.padAngle(0.05)
.padRadius(innerRadius))
// alter opactity of the labeling as well, after 2 seconds
}
var controller = new ScrollMagic.Controller();
new ScrollMagic.Scene({
// the element to scroll inside
triggerElement: '#grades_circular'
})
.on('enter', function(e) {
update_bars(e);
}).addTo(controller)
});
Thanks!
How would one attempt to get a random coordinate within an existing arc?
I am currently rendering a piechart from user data and would like to render a number of points on each arc at random locations - they may be outside of their arc, however
At the moment, I am using a random deviation (within a certain range) from the centroid of each arc. This approach is problematic as arcs might be too small and points end up outside of their arc.
I can't really provide any example code at the moment as I am really just rendering a pie chart with five slices so far.
I've used this example as a starting point. What I did was generate 10 times 2 numbers for every arc: a distance from the centre and an angle (in radians). Then I plotted the circles using those values.
To show that it works, I made the radius constant, so you see a circle of black dots. If you want, you can use the commented out line to make that random as well.
Note how the circles are the same colour as the arcs.
I also had to subtract Math.PI / 2, because of the different zero points between degrees and radians:
0 degrees is a vertical line to the top;
0 radians is a horizontal line to the right. -Math.PI / 2 radians is a vertical line to the top.
const data = [{
"region": "North",
"count": "53245"
},
{
"region": "South",
"count": "28479"
},
{
"region": "East",
"count": "19697"
},
{
"region": "West",
"count": "24037"
},
{
"region": "Central",
"count": "40245"
}
];
const width = 360;
const height = 360;
const radius = Math.min(width, height) / 2;
const svg = d3.select("#chart-area")
.append("svg")
.attr("width", width)
.attr("height", height)
.append("g")
.attr("transform", `translate(${width / 2}, ${height / 2})`);
const color = d3.scaleOrdinal(["#66c2a5", "#fc8d62", "#8da0cb",
"#e78ac3", "#a6d854", "#ffd92f"
]);
const pie = d3.pie()
.value(d => d.count)
.sort(null);
const arc = d3.arc()
.innerRadius(0)
.outerRadius(radius);
// Join new data
const path = svg.selectAll("path")
.data(pie(data));
// Enter new arcs
path.enter().append("path")
.attr("fill", (d, i) => color(i))
.attr("d", arc)
.attr("stroke", "white")
.attr("stroke-width", "6px")
.each(drawPoints);
function drawPoints(d, i) {
// Generate random numbers (x, y) where x between startAngle
// and endAngle
// and y between 0 and radius
const points = new Array(10).fill(undefined).map(() => ({
angle: d.startAngle + Math.random() * (d.endAngle - d.startAngle) - Math.PI / 2,
//radius: Math.random() * radius,
radius: radius / 2,
}));
svg.selectAll(`.point-${i}`)
.data(points)
.enter()
.append('circle')
.attr('class', `point point-${i}`)
.attr("fill", (d) => color(i))
.attr('stroke', 'black')
.attr('stroke-width', '2px')
.attr('cx', (d) => d.radius * Math.cos(d.angle))
.attr('cy', (d) => d.radius * Math.sin(d.angle))
.attr('r', 3)
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/6.2.0/d3.js"></script>
<div id="chart-area"></div>
I have a half donut I based a bit off http://bl.ocks.org/mikeyao/1c5c69b562cc4dc915a7af157e9c967e and some code I already had for a full donut chart, it currently that looks like this:
As shown in the image, the value is zero, but zero should not fill up half the chart. How can I set zero to start at the left most corner? Or in other words, how can I tell d3js that the chart should fill values from left corner to right corner. This is my code:
let initChart = function() {
let width = 148;
let height = 148;
let radius = Math.min(width, height) / 2;
let color = d3.scale.ordinal().range(scope.colors);
let selector = '#half-donut-' + scope.section;
let angle = 0.5 * Math.PI;
let data = [
{
label: 'Data',
value: _data
}
];
let backgroundArc = d3.svg
.arc()
.innerRadius(58)
.outerRadius(radius)
.cornerRadius(20)
.startAngle(angle * -1)
.endAngle(angle);
let mainArc = d3.svg
.arc()
.innerRadius(58)
.outerRadius(radius)
.cornerRadius(20)
.startAngle(angle * -1)
.endAngle(function(d) {
return (d.value / 100) * angle;
});
let svg = d3
.select(selector)
.append('svg')
.attr('width', width)
.attr('height', height);
let charts = svg
.selectAll('g')
.data(data)
.enter()
.append('g')
.attr('transform', function() {
return (
'translate(' + width / 2 + ',' + height / 2 + ')'
);
});
let legend = svg
.selectAll('.legend')
.data(data)
.enter()
.append('g')
.attr('class', 'legend')
.attr('transform', function() {
return 'translate(' + -21 + ',' + -21 + ')';
});
legend
.append('text')
.attr('x', 22)
.attr('y', 12)
.attr('text-anchor', 'middle')
.attr(
'transform',
'translate(' + width / 2 + ',' + height / 2 + ')'
)
.text(function(d) {
return d.value + '%';
});
charts
.append('path')
.attr('d', backgroundArc)
.attr('fill', '#F3F3F4');
charts
.append('path')
.attr('d', mainArc)
.attr('fill', color);
};
Thanks in advance.
Calc endAngle relative to startAngle
.endAngle(function(d) { return -angle + (d.value / 100) * 2 * angle; });
or use the pie method used in the example
I wanted to draw an arc from an array of points like this:
var points = [
[
51.93326250000001,
21.4375
],
[
36.72733749999999,
40.603550000000002
],
[
21.527537500000008,
21.4144
]
];
I tried with d3.line(), d3.curveBasis() and d3.curveBundle.beta(1).
var arcPath = d3.line()
.x(function (d) {
return d[0];
})
.y(function (d) {
return d[1];
})
.curve(d3.curveBasis);
var arc = node.append('path').attr("d", arcPath(points));
But it is drawing a curved line:
which is not what I am looking for. I would like an arc instead:
I don't understand how to use this:
var arc = d3.arc()
.innerRadius(180)
.outerRadius(240)
.startAngle(0);
with my points.
In order to draw an arc, you need to know the center coordinates of its associated circle and its radius.
In this case, as your arc (part of circle) is defined by the coordinates of 3 points, you need to compute the center of the circle defined by these 3 points:
var points = [
[
51.93326250000001,
21.4375
],
[
36.72733749999999,
40.603550000000002
],
[
21.527537500000008,
21.4144
]
];
function calculateCircleCenter(A, B, C) {
var yDelta_a = B[1] - A[1];
var xDelta_a = B[0] - A[0];
var yDelta_b = C[1] - B[1];
var xDelta_b = C[0] - B[0];
var center = [];
var aSlope = yDelta_a / xDelta_a;
var bSlope = yDelta_b / xDelta_b;
center[0] = (aSlope*bSlope*(A[1] - C[1]) + bSlope*(A[0] + B[0]) - aSlope*(B[0]+C[0]) )/(2* (bSlope-aSlope) );
center[1] = -1*(center[0] - (A[0]+B[0])/2)/aSlope + (A[1]+B[1])/2;
return center;
}
function distance(A, B) {
var a = A[0] - B[0];
var b = A[1] - B[1];
return Math.sqrt(a*a + b*b);
}
var center = calculateCircleCenter(points[0], points[1], points[2]);
var radius = distance(points[0], center);
var svg = d3.select("svg").attr("width", 200).attr("height", 200);
// The circle
svg.append("circle")
.attr("cx", center[0])
.attr("cy", center[1])
.attr("r", radius)
.attr("fill", "white")
.attr("stroke", "black");
var startAngle = Math.atan2(points[0][1] - center[1], points[0][0] - center[0]) + 0.5 * Math.PI;
var endAngle = Math.atan2(center[1] - points[2][1], center[0] - points[2][0]) + 1.5 * Math.PI;
var arc = d3.arc().innerRadius(radius).outerRadius(radius);
var sector = svg.append("path")
.attr("fill", "none")
.attr("stroke-width", 2)
.attr("stroke", "blue")
.attr("d", arc({ "startAngle": startAngle, "endAngle": endAngle }))
.attr("transform", "translate(" + center[0] + "," + center[1] + ")");
// The 3 points:
svg.selectAll("small_circle")
.data(points)
.enter().append("circle")
.attr("cx", function (d) { return d[0]; })
.attr("cy", function (d) { return d[1]; })
.attr("r", 2)
.attr("fill", "red");
<svg></svg>
<script src="https://d3js.org/d3.v4.min.js"></script>
Concerning the maths:
You can use whatever method to compute the center of a circle defined by 3 points. Here is used this one.
You can then compute the radius of this circle by computing the distance between this center and one of the three points.
And you will also need to know the start and end angles of the arc, based on the angle between the first point and the circle's center and the angle between the last point and the circle's center. This can be achieved using this formula.
Concerning the drawing:
Here is how you can draw an arc with d3.js:
var arc = d3.arc().innerRadius(radius).outerRadius(radius);
var sector = svg.append("path")
.attr("fill", "none")
.attr("stroke-width", 2)
.attr("stroke", "blue")
.attr("d", arc({ startAngle: 0.5 * Math.PI, endAngle: 1.5 * Math.PI }))
.attr("transform", "translate(" + center[0] + "," + center[1] + ")");
An arc is defined by its radius. More specifically its innerRadius and outerRadius. In our case it's the same thing.
We then specify the center of the arc by translating the arc:
.attr("transform", "translate(" + center[0] + "," + center[1] + ")");
And we specify the start and end angles of the arc this way:
.attr("d", arc({ "startAngle": startAngle, "endAngle": endAngle }))
where startAngle and endAngle are computed based on first/last points and the center:
var startAngle = Math.atan2(points[0][1] - center[1], points[0][0] - center[0]) + 0.5 * Math.PI;
var endAngle = Math.atan2(center[1] - points[2][1], center[0] - points[2][0]) + 1.5 * Math.PI;
I have a graph drawn with d3 with the x and y scales defined as follows:
x = d3.scaleLinear().domain([30, 150]).range([0, width])
y = d3.scaleLinear().domain([0, 100]).range([height, 0])
I need to draw a vertical line after 25% of data points. So I have my code like this:
svg.append('line')
.attr('x1', lowerLimit)
.attr('y1', 0)
.attr('x2', lowerLimit)
.attr('y2', height)
.style('stroke', 'red')
My problem is I am not sure how to set the lowerLimit x value to be 25% of all the x-values in the scale. Can someone help please? Thanks in advance!
Well, your question is not clear. When you say:
I need to draw a vertical line after 25% of data points
You're talking about the first quartile, which is impossible to calculate without the data, and not only that, but which changes for every different data set.
If you are indeed talking about the first quartile, this is what you have to do:
Given an data array called data, you can use d3.quantile:
var firstQuartile = d3.quantile(data, 0.25);
In the following demo, I'm plotting 100 dots, and calculating the 25 percentile (first quartile) regarding the property x:
var lowerLimit = d3.quantile(data, 0.25, function(d) {
return d.x
});
The console shows the value. Check the demo:
var data = d3.range(100).map(() => ({
x: Math.random() * 120 + 30,
y: Math.random() * 100
}));
var w = 500,
h = 160;
var svg = d3.select("body")
.append("svg")
.attr("width", w)
.attr("height", h);
x = d3.scaleLinear().domain([30, 150]).range([20, w - 20]);
y = d3.scaleLinear().domain([0, 100]).range([h - 20, 20]);
var xAxis = d3.axisBottom(x);
var yAxis = d3.axisLeft(y);
var circles = svg.selectAll("circles")
.data(data)
.enter()
.append("circle")
.attr("r", 2)
.attr("fill", "teal")
.attr("cx", d => x(d.x))
.attr("cy", d => y(d.y));
var data = data.sort((a, b) => d3.ascending(a.x, b.x))
var lowerLimit = d3.quantile(data, 0.25, function(d) {
return d.x
});
console.log(lowerLimit);
svg.append('line')
.attr('x1', x(lowerLimit))
.attr('y1', 20)
.attr('x2', x(lowerLimit))
.attr('y2', h - 20)
.style('stroke', 'red')
svg.append("g")
.attr("transform", "translate(0," + (h - 20) + ")")
.call(xAxis);
svg.append("g")
.attr("transform", "translate(20,0)")
.call(yAxis);
<script src="https://d3js.org/d3.v4.min.js"></script>
However, if you're talking about "getting the value that is 25% of the domain", the answer is easy. You can, for instance, create a function:
function findLimit(percentage) {
return x(x.domain()[0] + (x.domain()[1] - x.domain()[0]) * percentage / 100);
};
And pass the value of that function to lowerLimit:
var lowerLimit = findLimit(25);
Check this demo, drawing a line at 25% of the x axis:
var data = d3.range(100).map(() => ({
x: Math.random() * 120 + 30,
y: Math.random() * 100
}));
var w = 500,
h = 200;
var svg = d3.select("body")
.append("svg")
.attr("width", w)
.attr("height", h);
x = d3.scaleLinear().domain([30, 150]).range([20, w - 20]);
y = d3.scaleLinear().domain([0, 100]).range([h - 20, 20]);
var xAxis = d3.axisBottom(x);
var yAxis = d3.axisLeft(y);
var circles = svg.selectAll("circles")
.data(data)
.enter()
.append("circle")
.attr("r", 2)
.attr("fill", "teal")
.attr("cx", d => x(d.x))
.attr("cy", d => y(d.y));
var data = data.sort((a, b) => d3.ascending(a.x, b.x))
var lowerLimit = d3.quantile(data, 0.25, function(d) {
return d.x
});
console.log(lowerLimit);
svg.append('line')
.attr('x1', x(lowerLimit))
.attr('y1', 20)
.attr('x2', x(lowerLimit))
.attr('y2', h - 20)
.style('stroke', 'red')
svg.append("g")
.attr("transform", "translate(0," + (h - 20) + ")")
.call(xAxis);
svg.append("g")
.attr("transform", "translate(20,0)")
.call(yAxis);
<script src="https://d3js.org/d3.v4.min.js"></script>