I worked in the d3 library and created nested wheels. I have no idea how to add texts into the wheel, spirally from inside out. The number starting point doesn't matter, and numbers must spirally outwards according the previous position.
Codes
let allAxis = (data.map(function(i, j) {
return i.name
})),
total = allAxis.length,
radius = Math.min(options.width / 2, options.height / 2),
angleSlice = Math.PI * 2 / total,
Format = d3.format('');
let rScale = d3.scale.linear()
.domain([0, options.circles.maxValue])
.range([50, radius]);
let svg = d3.select("body").append("svg")
.attr("width", options.width + options.margins.left + options.margins.right)
.attr("height", options.height + options.margins.top + options.margins.bottom);
let g = svg.append("g")
.attr("transform", "translate(" + (options.width / 2 + options.margins.left) + "," + (options.height / 2 + options.margins.top) + ")");
let axisGrid = g.append("g")
.attr("class", "axisWraper");
let axis = axisGrid.selectAll(".axis")
.data(allAxis)
.enter()
.append("g")
.attr("class", "axis")
//append them lines
axis.append("line")
.attr("x1", 0)
.attr("y1", 0)
.attr("x2", function(d, i) {
let tempX2 = radius * Math.cos(angleSlice * i - Math.PI / 2);
return tempX2;
})
.attr("y2", function(d, i) {
let tempY = radius * Math.sin(angleSlice * i - Math.PI / 2);
return tempY;
})
.attr("class", "line")
.attr("stroke", "black")
.attr("fill", "none");
//Draw background circles
axisGrid.selectAll(".levels")
.data([12,11,10,9,8,7,6, 5, 4, 3, 2, 1])
.enter()
.append("circle")
.attr("class", function(d, i) {
return `gridCircle-${d}`
})
.attr("r", function(d, i) {
return parseInt(radius / options.circles.levels * d, 10);
})
.attr("stroke", "black")
.attr("fill-opacity", function(d, i) {
return options.circles.opacity;
});
axisGrid.select(".gridCircle-1").attr("fill-opacity", 1);
axisGrid.select(".gridCircle-2").attr("fill-opacity", 1);
Expected Result
Updated #1 (with PointRadial)
Here's the fiddle: http://jsfiddle.net/arcanabliss/8yjacdoz/73/
You need pointRadial:
Returns the point [x, y] for the given angle in radians, with 0 at -y
(12 o’clock) and positive angles proceeding clockwise, and the given
radius.
You can play around with the example below in order to fit to your arrangement e.g. in order to start from 3 o'clock and rotate counter-clockwise etc you just need to play with degrees and radius:
const degrees = (i % numbersPerRing) * (360 / numbersPerRing);
const radius = (Math.floor(i / numbersPerRing) + 1) * ringSize;
// numbers
const numbersPerRing = 8;
const ringSize = 30;
const originX = 120;
const originY = 120;
const numbers = [... new Array(18)].map((d, i) => i + 1);
const diameters = [... new Array(4)].map((d, i) => (i * ringSize) + 10);
const points = numbers.map((n, i) => {
const degrees = (i % numbersPerRing) * (360 / numbersPerRing);
const radius = (Math.floor(i / numbersPerRing) + 1) * ringSize;
const point = d3.pointRadial(degrees * (Math.PI / 180), radius);
return {
"value": n,
"degrees": degrees,
"point": point
}
});
// svg
const svg = d3.select("body")
.append("svg")
.attr("width", originX * 2)
.attr("height", originY * 2);
// circles
svg.selectAll("circles")
.data(diameters)
.enter()
.append("circle")
.attr("cx", originX)
.attr("cy", originY)
.attr("r", d => d)
.attr("fill", "none")
.attr("stroke", "#aaaaaa");
// labels
svg.selectAll("labels")
.data(points)
.enter()
.append("text")
.attr("x", (d, i) => d.point[0])
.attr("y", (d, i) => d.point[1])
.attr("transform", `translate(${originX},${originX})`)
.attr("text-anchor", (d) => (d.degrees > 180) ? "end" : "start")
.text(d => d.value);
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
Related
I try to do a beeswarm plot with different radius; inspired by this code
The issue I have, is that my point are offset regarding my x axis:
The point on the left should be at 31.7%. I don't understand why, so I would appreciate if you could guide me. This could be improved by changing the domain of x scale, but this can't match the exact value; same issue if I remove the d3.forceCollide()
Thank you,
Data are available here.
Here is my code:
$(document).ready(function () {
function tp(d) {
return d.properties.tp60;
}
function pop_mun(d) {
return d.properties.pop_mun;
}
var margin = {top: 20, right: 20, bottom: 20, left: 40},
width = 1280 - margin.right - margin.left,
height = 300 - margin.top - margin.bottom;
var svg = d3.select("body")
.append("svg")
.attr("viewBox", `0 0 ${width} ${height}`)
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
var z = d3.scaleThreshold()
.domain([.2, .3, .4, .5, .6, .7])
.range(["#35ff00", "#f1a340", "#fee0b6",
"#ff0000", "#998ec3", "#542788"]);
var loading = svg.append("text")
.attr("x", (width) / 2)
.attr("y", (height) / 2)
// .attr("dy", ".35em")
.style("text-anchor", "middle")
.text("Simulating. One moment please…");
var formatPercent = d3.format(".0%"),
formatNumber = d3.format(".0f");
d3.json('static/data/qp_full.json').then(function (data) {
features = data.features
//1 create scales
var x = d3.scaleLinear()
.domain([0, d3.max(features, tp)/100])
.range([0, width - margin.right])
var y = d3.scaleLinear().domain([0, 0.1]).range([margin.left, width - margin.right])
var r = d3.scaleSqrt().domain([0, d3.max(features, pop_mun)])
.range([0, 25]);
//2 create axis
var xAxis = d3.axisBottom(x).ticks(20)
.tickFormat(formatPercent);
svg.append("g")
.attr("class", "x axis")
.call(xAxis);
var nodes = features.map(function (node, index) {
return {
radius: r(node.properties.pop_mun),
color: '#ff7f0e',
x: x(node.properties.tp60 / 100),
y: height + Math.random(),
pop_mun: node.properties.pop_mun,
tp60: node.properties.tp60
};
});
function tick() {
for (i = 0; i < nodes.length; i++) {
var node = nodes[i];
node.cx = node.x;
node.cy = node.y;
}
}
setTimeout(renderGraph, 10);
function renderGraph() {
// Run the layout a fixed number of times.
// The ideal number of times scales with graph complexity.
// Of course, don't run too long—you'll hang the page!
const NUM_ITERATIONS = 1000;
var force = d3.forceSimulation(nodes)
.force('charge', d3.forceManyBody().strength(-3))
.force('center', d3.forceCenter(width / 2, height/2))
.force('x', d3.forceX(d => d.x))
.force('y', d3.forceY(d => d.y))
.force('collide', d3.forceCollide().radius(d => d.radius))
.on("tick", tick)
.stop();
force.tick(NUM_ITERATIONS);
force.stop();
svg.selectAll("circle")
.data(nodes)
.enter()
.append("circle")
.attr("cx", d => d.x)
.attr("cy", d => d.y)
.attr("r", d => d.radius)
.style("fill", d => z(d.tp60/100))
.on("mouseover", function (d, i) {
d3.select(this).style('fill', "orange")
console.log(i.tp60,i)
svg.append("text")
.attr("id", "t")
.attr("x", function () {
return d.x - 50;
})
.attr("y", function () {
return d.y - 50;
})
.text(function () {
return [x.invert(i.x), i.tp60]; // Value of the text
})
})
.on("mouseout", function (d, i) {
d3.select("#t").remove(); // Remove text location
console.log(i)
d3.select(this).style('fill', z(i.tp60/100));
});
loading.remove();
}
})
})
I have a d3 (v7) visualization where I have a variable number of circles being drawn on the screen depending on my data set.
How can I get arrows connecting these circles? I'm trying to follow this guide: https://observablehq.com/#harrylove/draw-an-arrow-between-circles-with-d3-links
However, this is only for a set number of circles (2) and I will have a variable number of them depending on my dataset.
Below is my current d3 code that draws circles:
var svgContainer = d3.select("body")
.append("svg")
.attr("width", 800)
.attr("height", 200);
var circles = svgContainer.selectAll("circle")
.data(nodeObjs)
.enter()
.append("circle");
circles
.attr("cx", function (d, i) {return i * 100 + 30})
.attr("cy", 60)
.attr("r", 30)
.style("fill", "steelblue");
To make the observable example dynamic there are a few factors to take into account:
you need 2 link functions; 1 for horizontal and 1 for vertical - below I have linkH and linkV instead of just link
the link function doesn't need to be called immediately so lose the ({ source: linkSource, target: linkTarget}); - you are going to need an array of links instead
some choice between linkH and linkV - you can test if the x-gap is greater than the y-gap between two circles and choose a horizontal link; and vice versa
in the example I've made the horizontal vs vertical decision in the link creation; then call linkH or linkV in the .attr("d", ...) section
in the case that the arrow runs right to left or top to bottom you need to reverse the sign on the adjustment of the link x and y
See the working example below:
const svgWidth = 480;
const svgHeight = 180;
const svg = d3.select("body")
.append("svg")
.attr("width", svgWidth)
.attr("height", svgHeight);
// Define the arrowhead marker variables
const markerBoxWidth = 8;
const markerBoxHeight = 8;
const refX = markerBoxWidth / 2;
const refY = markerBoxHeight / 2;
const markerWidth = markerBoxWidth / 2;
const markerHeight = markerBoxHeight / 2;
const arrowPoints = [[0, 0], [0, 8], [8, 4]];
// Add the arrowhead marker definition to the svg element
svg
.append("defs")
.append("marker")
.attr("id", "arrow")
.attr("viewBox", [0, 0, markerBoxWidth, markerBoxHeight])
.attr("refX", refX)
.attr("refY", refY)
.attr("markerWidth", markerBoxWidth)
.attr("markerHeight", markerBoxHeight)
.attr("orient", "auto-start-reverse")
.append("path")
.attr("d", d3.line()(arrowPoints))
.attr("stroke", "black");
// horizontal link
const linkH = d3
.linkHorizontal()
.x(d => d.x)
.y(d => d.y);
// vertical link
const linkV = d3
.linkVertical()
.x(d => d.x)
.y(d => d.y);
// circle data
const n = (Math.floor(Math.random() * 12) * 2) + 2;
const circleRadius = 10;
const nodes = [];
const links = [];
for (let i=0; i<n; i++) {
nodes.push({
x: Math.floor(Math.random() * (svgWidth - 20)) + 20,
y: Math.floor(Math.random() * (svgHeight - 20)) + 20,
r: circleRadius
});
}
for (let i=0; i<n; i+=2) {
const xdelta = Math.abs(nodes[i + 1].x - nodes[i].x);
const ydelta = Math.abs(nodes[i + 1].y - nodes[i].y);
links.push({
source: { x: nodes[i].x, y: nodes[i].y },
target: { x: nodes[i + 1].x, y: nodes[i + 1].y },
arrowDirection: ydelta >= xdelta ? "V" : "H"
});
}
const circles = svg.selectAll(".node")
.data(nodes)
.enter()
.append("circle")
.attr("class", "node");
circles
.attr("cx", (d, i) => d.x)
.attr("cy", (d, i) => d.y)
.attr("r", d => d.r);
const arrows = svg.selectAll(".arrow")
.data(links)
.enter()
.append("path")
.attr("class", "arrow");
arrows
.attr("d", (d, i) => {
let reversed;
if (d.arrowDirection === "H") {
reversed = d.source.x < d.target.x ? 1 : -1;
d.source.x += circleRadius * reversed;
d.target.x -= (circleRadius + markerWidth) * reversed;
return linkH(d);
} else {
reversed = d.source.y > d.target.y ? 1 : -1;
d.source.y -= circleRadius * reversed;
d.target.y += (circleRadius + markerWidth) * reversed;
return linkV(d);
}
})
.attr("marker-end", "url(#arrow)");
.node {
fill: green;
stroke: steelblue;
}
.arrow {
stroke: black;
fill: none;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/7.0.0/d3.min.js"></script>
I'm trying to create a donut chart in d3js where each arc has a circle at its end.
Circle's edge must fit on arc's one.
I tried both by appending a circle and a circle wrapped in marker but with no succes.
Trying to append a marker seems to be the closest solution to the desired one but I can't help the marker oveflowing the arc edges.
Code:
var data = [
{
name: "punti",
count: 3,
color: "#fff000"
},
{
name: "max",
count: 7,
color: "#f8b70a"
}
];
var totalCount = data.reduce((acc, el) => el.count + acc, 0);
var image_width = 32;
var image_height = 32;
var width = 540,
height = 540,
radius = 200,
outerRadius = radius - 10,
innerRadius = 100;
var cornerRadius = innerRadius;
var markerRadius = (outerRadius - innerRadius) / 2;
var arc = d3
.arc()
.outerRadius(outerRadius)
.innerRadius(innerRadius)
.cornerRadius(cornerRadius);
var pie = d3
.pie()
.sort(null)
.value(function(d) {
return d.count;
});
var svg = d3
.select("body")
.append("svg")
.attr("width", width)
.attr("height", height)
.append("g")
.attr("transform", "translate(" + width / 2 + "," + height / 2 + ")");
var pieData = pie(data);
var g = svg
.selectAll(".arc")
.data(pieData)
.enter()
.append("g");
var path = g
.append("path")
.attr("d", arc)
.style("fill", function(d, i) {
return d.data.color;
});
var marker = svg
.append("defs")
.append("marker")
.attr("id", "endmarker")
.attr("overflow", "visible")
.append("circle")
.attr("cy", 0)
.attr("cx", 0)
.attr("r", markerRadius)
.attr("fill", "red");
g.attr("marker-end", "url(#endmarker)");
g
.append("circle")
.attr("cx", function(d) {
let path = d3.select(this.parentNode);
var x = arc.centroid(d)[0];
return x;
})
.attr("cy", function(d) {
var y = arc.centroid(d)[1];
console.log(d3.select(this).attr("cx"));
return y;
})
.attr("fill", d => d.data.color)
.attr("stroke", "black")
.attr("r", (outerRadius - innerRadius) / 2);
codepen here
Thanks to anyone who will help!
Assuming that you want your output like:
I found some code from Mike Bostock's Block here which shows how to add circles to rounded Arc Corners.
I adapted the following code for you which performs quite a bit of complex mathematics.
var cornerRadius = (outerRadius - innerRadius)/2;
svg.append("g")
.style("stroke", "#555")
.style("fill", "none")
.attr("class", "corner")
.selectAll("circle")
.data(d3.merge(pieData.map(function(d) {
return [
{angle: d.startAngle + d.padAngle / 2, radius: outerRadius - cornerRadius, start: +1},
{angle: d.endAngle - d.padAngle / 2, radius: outerRadius - cornerRadius, start: -1},
];
})))
.enter().append("circle")
.attr("cx", function(d) { return d.start * cornerRadius * Math.cos(d.angle) + Math.sqrt(d.radius * d.radius - cornerRadius * cornerRadius) * Math.sin(d.angle); })
.attr("cy", function(d) { return d.start * cornerRadius * Math.sin(d.angle) - Math.sqrt(d.radius * d.radius - cornerRadius * cornerRadius) * Math.cos(d.angle); })
.attr("r", cornerRadius);
Full snippet showing the output:
<div id="chart"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.12.0/d3.min.js"></script>
<script>
var data = [
{
name: "punti",
count: 3,
color: "#fff000"
},
{
name: "max",
count: 7,
color: "#f8b70a"
},
];
var totalCount = data.reduce((acc, el) => el.count + acc, 0);
var image_width = 32;
var image_height = 32;
var width = 540,
height = 540,
radius = 200,
outerRadius = radius - 10,
innerRadius = 100;
var cornerRadius = innerRadius;
var markerRadius = (outerRadius - innerRadius) / 2;
var arc = d3
.arc()
.outerRadius(outerRadius)
.innerRadius(innerRadius)
.cornerRadius(cornerRadius);
var pie = d3
.pie()
.sort(null)
.value(function(d) {
return d.count;
});
var svg = d3
.select("body")
.append("svg")
.attr("width", width)
.attr("height", height)
.append("g")
.attr("transform", "translate(" + width / 2 + "," + height / 2 + ")");
var pieData = pie(data);
var g = svg
.selectAll(".arc")
.data(pieData)
.enter()
.append("g");
var path = g
.append("path")
.attr("d", arc)
.style("fill", function(d, i) {
return d.data.color;
});
var cornerRadius = (outerRadius - innerRadius)/2;
svg.append("g")
.style("stroke", "#555")
.style("fill", "none")
.attr("class", "corner")
.selectAll("circle")
.data(d3.merge(pieData.map(function(d) {
return [
{angle: d.startAngle + d.padAngle / 2, radius: outerRadius - cornerRadius, start: +1},
{angle: d.endAngle - d.padAngle / 2, radius: outerRadius - cornerRadius, start: -1},
];
})))
.enter().append("circle")
.attr("cx", function(d) { return d.start * cornerRadius * Math.cos(d.angle) + Math.sqrt(d.radius * d.radius - cornerRadius * cornerRadius) * Math.sin(d.angle); })
.attr("cy", function(d) { return d.start * cornerRadius * Math.sin(d.angle) - Math.sqrt(d.radius * d.radius - cornerRadius * cornerRadius) * Math.cos(d.angle); })
.attr("r", cornerRadius);
</script>
My customized d3 radial chart is not rendering arc for least value.
Fiddle
var width = 360,
height = 300,
barHeight = height / 2 - 40;
var formatNumber = d3.format("s");
var color = d3.scale.ordinal()
.range(["#F15D5D","#FAD64B"]);
var svg = d3.select('#chart').append("svg")
.attr("width", width)
.attr("height", height)
.attr('class','radial')
.append("g")
.attr("transform", "translate(" + width/2 + "," + height/2 + ")");
var data = [{
"name": "ABC",
"value":4
},
{
"name": "XYZ",
"value":5
},{
"name": "DEF",
"value":2
},
{
"name": "GHI",
"value":3
},{
"name": "JKL",
"value":1
}];
data.sort(function(a,b) { return b.value - a.value; });
var extent = d3.extent(data, function(d) { return d.value; });
var barScale = d3.scale.linear()
.domain(extent)
.range([0, barHeight]);
var keys = data.map(function(d,i) { return d.name; });
var numBars = keys.length;
// X scale
var x = d3.scale.linear()
.domain(extent)
.range([0, -barHeight]);
// X axis
var xAxis = d3.svg.axis()
.scale(x).orient("left")
.ticks(3)
.tickFormat(formatNumber);
// Inner circles
var circles = svg.selectAll("circle")
.data(x.ticks(5))
.enter().append("circle")
.attr("r", function(d) {return barScale(d);})
.style("fill", "none")
//.style("stroke", "black")
//.style("stroke-dasharray", "2,2")
.style("stroke-width",".5px");
// Create arcs
var arc = d3.svg.arc()
.startAngle(function(d,i) {
var a = (i * 2 * Math.PI) / numBars;
var b = ((i + 1) * 2 * Math.PI) / numBars;
var d = (b-a) / 4;
var x = a+d;
var y = b-d;
return x;//(i * 2 * Math.PI) / numBars;
})
.endAngle(function(d,i) {
var a = (i * 2 * Math.PI) / numBars;
var b = ((i + 1) * 2 * Math.PI) / numBars;
var d = (b-a) / 4;
var x = a+d;
var y = b-d;
return y;//((i + 1) * 2 * Math.PI) / numBars;
})
.innerRadius(0);
// Render colored arcs
var segments = svg.selectAll("path")
.data(data)
.enter().append("path")
.each(function(d) { d.outerRadius = 0; })
.style("fill", function (d) { return color(d.name); })
.attr("d", arc);
segments.transition().ease("elastic").duration(1000).delay(function(d,i) {return (25-i)*50;})
.attrTween("d", function(d,index) {
var i = d3.interpolate(d.outerRadius, barScale(+d.value));
return function(t) { d.outerRadius = i(t); return arc(d,index); };
});
// Outer circle
svg.append("circle")
.attr("r", barHeight)
.classed("outer", true)
.style("fill", "none")
//.style("stroke", "black")
.style("stroke-width",".5px");
// Apply x axis
svg.append("g")
.attr("class", "x axis")
.call(xAxis);
// Labels
var labelRadius = barHeight * 1.025;
var labels = svg.append("g")
.classed("labels", true);
labels.append("def")
.append("path")
.attr("id", "label-path")
.attr("d", "m0 " + -labelRadius + " a" + labelRadius + " " + labelRadius + " 0 1,1 -0.01 0");
labels.selectAll("text")
.data(keys)
.enter().append("text")
.style("text-anchor", "middle")
.style("font-weight","bold")
.style("fill", function(d, i) {return "#555";})
.append("textPath")
.attr("xlink:href", "#label-path")
.attr("startOffset", function(d, i) {return i * 100 / numBars + 50 / numBars + '%';})
.text(function(d) {return d.toUpperCase(); });
Here the item JKL with value 1 is not rendered and the outerRadius is calculated as 0.
The problem lies here:
var extent = d3.extent(data, function(d) { return d.value; });
This line sets the domain of barScale and x. However, if you do this using d3.extent, you're actually saying that the domain goes from the minimum value to the maximum value...
[1, 5];
... meaning that the value 1 will be mapped to the minimum value of the range.
Instead of that, set the minimum value in the domain to zero:
var extent = [0, d3.max(data, d=>d.value)];
Which will have this result:
[0, 5];
Here is your updated fiddle: https://jsfiddle.net/a9w7c2dw/
I am developing a d3 sunburst type.
Everything is ok, It is taking the flare JSON correctly but, when I go to label the path look what is happening:
The code is the following:
var width = 960,
height = 700,
radius = Math.min(width, height) / 2;
var x = d3.scale.linear()
.range([0, 2 * Math.PI]);
var y = d3.scale.linear()
.range([0, radius]);
var hue = d3.scale.ordinal().range(["#feec76","#aec7e8","#ff00bf","#7f7f7f"]);
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height)
.append("g")
.attr("transform", "translate(" + width / 2 + "," + (height / 2 + 10) + ")");
var partition = d3.layout.partition()
.value(function(d) { return d.size; });
var arc = d3.svg.arc()
.startAngle(function(d) { return Math.max(0, Math.min(2 * Math.PI, x(d.x))); })
.endAngle(function(d) { return Math.max(0, Math.min(2 * Math.PI, x(d.x + d.dx))); })
.innerRadius(function(d) { return Math.max(0, y(d.y)); })
.outerRadius(function(d) { return Math.max(0, y(d.y + d.dy)); });
d3.json("http://api.printoriente.com/treemap.php", function(error, root) {
var g = svg.selectAll("g")
.data(partition.nodes(root))
.enter().append("g");
var path = g.append("path")
.attr("d", arc)
.style("fill", function(d) { return hue((d.children ? d : d.parent).name); })
.on("click", click);
var text = g.append("text")
.attr("transform", function(d) { return "rotate(" + computeTextRotation(d) + ")"; })
.attr("x", function(d) { return y(d.y); })
.attr("dx", "6") // margin
.attr("dy", ".35em") // vertical-align
.text(function(d) { return d.name; });
And the rotation code is:
function computeTextRotation(d) {
return (x(d.x + d.dx / 2) - Math.PI / 2) / Math.PI * 180;
}
This script works for all others d3 but I have to put those colors for each path.
Where is the problem?
Regards.
UPDATED: d3 sunburst with small font-size:
UPDATED: I want something like this:
UPDATED: Take a look of internal labels:
I'm not sure where you got the code to calculate the angle from, but it seems to be completely off. If you look at this example, the code to compute the angle is (with everything else being equal)
var angle = (d.x + d.dx / 2) * 180 / Math.PI - 90;
Replacing the code in your example with that fixes the angles. To fix the positions, you can adjust the dx offset of the labels, e.g.
.attr("dx", 50")
Complete example here.