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>
Related
I have a graph of lines paths .
Actually when I zoom it applies a transform attribute.
I would like to make a semantic zoom by zooming only on the x-axis.
Here's the code for zooming
private manageZoom(svgs: AllSvg, allAxis: AllAxis, dimension: Dimension): D3ZoomBehavior {
const zoom: D3ZoomBehavior = d3
.zoom()
.scaleExtent([1, 40])
.translateExtent([
[0, 0],
[dimension.width, dimension.height]
])
.on('zoom', zoomed.bind(null, allAxis));
svgs.svgContainer.call(zoom);
return zoom;
function zoomed({ xAxis, xAxisBottom, yAxis }: AllAxis, { transform }: any) {
svgs.sillons.attr('transform', transform);
xAxisBottom.axisContainer.call(xAxisBottom.axis.scale(transform.rescaleX(xAxisBottom.scale)) as any);
}
}
the sillons object is an array of paths + text + circles
I would that the lines get re-drawed in the right position as the x-axis get larger, but not zoom sillons on y-axis.
I have checked many posts but can't repoduce them to solve my issue. for example
When you set up something along the lines of
svg.call(
d3.zoom()
.on("zoom", zoom)
)
the zoom function can be just about anything that you want. The first argument of zoom is the zoom event itself. Let's denote it by evt. Then
evt.transform.k tells you the scale factor,
evt.transform.x tells you the horizontal translation, and
evt.transform.y tells you the vertical translation.
You don't have to use all of those, though. Rather, you can redraw your image however you want.
Here's a slightly cute example that rescales the image only horizontally.
let w = 500;
let h = 100;
let svg = d3
.select("#container")
.append("svg")
.attr("width", w)
.attr("height", h)
.style("border", "solid 1px black");
let n = 500;
let pts0 = d3.range(n).map((_) => [d3.randomNormal(w / 2, w / 20)(), 0]);
let pts1 = pts0.map((pt) => [w - pt[0], h]);
let g = svg.append("g");
let link_group = g.append("g");
link_group
.selectAll("path")
.data(d3.range(n))
.join("path")
.attr("d", (i) => d3.linkVertical()({ source: pts0[i], target: pts1[i] }))
.attr("fill", "none")
.attr("stroke", "#000")
.attr("stroke-opacity", 0.1)
.attr("stroke-width", 1.5);
let all_pts = pts0.concat(pts1);
let circle_group = g.append("g");
let circles = circle_group
.attr("fill", "black")
.attr("fill-opacity", 0.2)
.selectAll("circle")
.data(all_pts)
.join("circle")
.attr("cx", (d) => d[0])
.attr("cy", (d) => d[1])
.attr("data-x", (d) => d[0])
.attr("r", 4);
svg.call(
d3
.zoom()
.scaleExtent([1 / 4, 20])
.duration(500)
.on("zoom", function (evt) {
let k = evt.transform.k;
link_group.selectAll("path").attr("d", function (i) {
let x00 = pts0[i][0];
let x01 = k * (x00 - w / 2) + w / 2;
let x10 = pts1[i][0];
let x11 = k * (x10 - w / 2) + w / 2;
return d3.linkVertical()({ source: [x01, 0], target: [x11, h] });
});
circle_group
.selectAll("circle")
.nodes()
.forEach(function (c) {
let x0 = c.getAttribute("data-x");
let k = evt.transform.k;
let x1 = k * (x0 - w / 2) + w / 2;
c.setAttribute("cx", x1);
});
})
);
<script src="https://d3js.org/d3.v7.min.js"></script>
<div id="container"></div>
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>
I use d3js to draw a smooth curve. Then, I want to draw a point on the curve, but the point is random and I only have x value. I want get the function expression and get the y value with the x value. Is there any method to get the y value?
const line = d3.line()
.x(d => xScale(new Date(d.name)))
.y(d => yScale(d.value1))
.curve(d3.curveCatmullRom);
const series = svg.append('g')
.attr('transform', `translate(${grid.left},${grid.top})`)
.append('path')
.attr('d', line(data))
.attr('fill', 'transparent')
.attr('stroke-width', 2)
.attr('stroke', 'orange');
My current chart:
Here is a function that finds a point with specified x coordinate on a <path> (kind of binary search):
Note: The path should be monotonic on X (there must not be 2 points with the same x on the path)
const findPointAt = (path, x) => {
let from = 0;
let to = path.getTotalLength();
let current = (from + to) / 2;
let point = path.getPointAtLength(current);
while (Math.abs(point.x - x) > 0.5) {
if (point.x < x)
from = current;
else
to = current;
current = (from + to) / 2;
point = path.getPointAtLength(current);
}
return point;
}
const path = d3.select('path').node();
for (let x = 0; x <= 200; x += 50) {
const pos = findPointAt(path, x);
console.log(pos);
d3.select('svg').append('circle')
.attr('cx', pos.x)
.attr('cy', pos.y)
.attr('r', 3)
}
svg {
border: 1px solid gray;
}
path {
fill: none;
stroke: blue;
}
circle {
fill: red;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<svg width="200" height="150">
<path d="M 0,10 Q 40,0 90,80 C 120,120 150,70 220,20" />
</svg>
It's really a duplicate of this but I added a snippet as the post is several years old ...
const margin = 30;
const width = 400;
const height = 180;
const chartWidth = width - (margin * 2);
const chartHeight = height - (margin * 2);
const data = Array.from({length: 10}, (v, i) => {
return {
index: i,
value: Math.floor(Math.random() * 20) + 4
}
});
const svg = d3.select("#viz")
.append("svg")
.attr("width", width)
.attr("height", height);
const xScale = d3.scaleLinear()
.domain(d3.extent(data, d => d.index))
.range([0, chartWidth]);
svg.append("g")
.attr("class", "x-axis")
.attr("transform", `translate(${margin},${height - margin})`)
.call(d3.axisBottom(xScale));
const yScale = d3.scaleLinear()
.domain(d3.extent(data, d => d.value))
.range([chartHeight, 0]);
svg.append("g")
.attr("class", "y-axis")
.attr("transform", `translate(${margin},${margin})`)
.call(d3.axisLeft(yScale));
const line = d3.line()
.x(d => xScale(d.index))
.y(d => yScale(d.value))
.curve(d3.curveCatmullRom);
const series = svg.append("g")
.attr("transform", `translate(${margin},${margin})`)
.append("path")
.attr("d", line(data))
.attr("fill", "transparent")
.attr("stroke-width", 2)
.attr("stroke", "orange");
const findYFromXLinearTime = (x, line) => {
const getXYAtLength = len => {
const pt = line.getPointAtLength(len);
return {x: pt.x, y: pt.y};
}
let l = 0;
while (getXYAtLength(l).x < x) l+=0.01;
return getXYAtLength(l).y;
}
const findYFromXLogTime = (x, line) => {
const error = 0.01;
const iterMax = 50;
let iter = 0;
let start = 0;
let end = line.getTotalLength();
let point = line.getPointAtLength((end + start) / 2);
while (x < point.x - error || x > point.x + error) {
// update middle
point = line.getPointAtLength((end + start) / 2);
// test
x < point.x ? end = (start + end) / 2 : start = (start + end ) / 2;
// update iteration
if (iterMax < ++ iter) break;
}
return point.y;
}
d3.select("#findY")
.on("click", evt => {
const x = document.getElementById("someX").value;
const y = findYFromXLogTime(xScale(x), series.node());
svg.append("circle")
.attr("cx", xScale(x) + margin)
.attr("cy", y + margin)
.attr("r", 4)
.attr("fill", "red")
.attr("stroke", "black")
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/6.7.0/d3.min.js"></script>
<input id="someX" type="number">
<button id="findY" type="button">find Y</button>
<div id="viz"></div>
I'm trying to make my map look this way
Unfortunately, my code looks this way, and I don't understand why my text nodes are so gigantic not the way I want it
this is the code that I have going or check my fiddle
This code specifically doesn't seem to produce human readable labels
grp
.append("text")
.attr("fill", "#000")
.style("text-anchor", "middle")
.attr("font-family", "Verdana")
.attr("x", 0)
.attr("y", 0)
.attr("font-size", "10")
.text(function (d, i) {
return name;
});
Here's my full code:
var width = 500,
height = 275,
centered;
var projection = d3.geo
.conicConformal()
.rotate([103, 0])
.center([0, 63])
.parallels([49, 77])
.scale(500)
.translate([width / 2.5, height / 2])
.precision(0.1);
var path = d3.geo.path().projection(projection);
var svg = d3
.select("#map-selector-app")
.append("svg")
.attr("viewBox", `0 0 ${width} ${height}`);
// .attr("width", width)
// .attr("height", height);
svg
.append("rect")
.attr("class", "background-svg-map")
.attr("width", width)
.attr("height", height)
.on("click", clicked);
var g = svg.append("g");
var json = null;
var subregions = {
Western: { centroid: null },
Prairies: { centroid: null },
"Northern Territories": { centroid: null },
Ontario: { centroid: null },
Québec: { centroid: null },
Atlantic: { centroid: null },
};
d3.json(
"https://gist.githubusercontent.com/KatFishSnake/7f3dc88b0a2fa0e8c806111f983dfa60/raw/7fff9e40932feb6c0181b8f3f983edbdc80bf748/canadaprovtopo.json",
function (error, canada) {
if (error) throw error;
json = topojson.feature(canada, canada.objects.canadaprov);
g.append("g")
.attr("id", "provinces")
.selectAll("path")
.data(json.features)
.enter()
.append("path")
.attr("d", path)
.on("click", clicked);
g.append("g")
.attr("id", "province-borders")
.append("path")
.datum(
topojson.mesh(canada, canada.objects.canadaprov, function (
a,
b
) {
return a !== b;
})
)
.attr("id", "province-borders-path")
.attr("d", path);
// g.select("g")
// .selectAll("path")
// .each(function (d, i) {
// var centroid = path.centroid(d);
// });
Object.keys(subregions).forEach((rkey) => {
var p = "";
json.features.forEach(function (f, i) {
if (f.properties.subregion === rkey) {
p += path(f);
}
});
var tmp = svg.append("path").attr("d", p);
subregions[rkey].centroid = getCentroid(tmp.node());
subregions[rkey].name = rkey;
tmp.remove();
});
Object.values(subregions).forEach(({ centroid, name }) => {
var w = 80;
var h = 30;
var grp = g
.append("svg")
// .attr("width", w)
// .attr("height", h)
.attr("viewBox", `0 0 ${w} ${h}`)
.attr("x", centroid[0] - w / 2)
.attr("y", centroid[1] - h / 2);
// grp
// .append("rect")
// .attr("width", 80)
// .attr("height", 27)
// .attr("rx", 10)
// .attr("fill", "rgb(230, 230, 230)")
// .attr("stroke-width", "2")
// .attr("stroke", "#FFF");
grp
.append("text")
.attr("fill", "#000")
.style("text-anchor", "middle")
.attr("font-family", "Verdana")
.attr("x", 0)
.attr("y", 0)
.attr("font-size", "10")
.text(function (d, i) {
return name;
});
// var group = g.append("g");
// group
// .append("rect")
// .attr("x", centroid[0] - w / 2)
// .attr("y", centroid[1] - h / 2)
// .attr("width", 80)
// .attr("height", 27)
// .attr("rx", 10)
// .attr("fill", "rgb(230, 230, 230)")
// .attr("stroke-width", "2")
// .attr("stroke", "#FFF");
// group
// .append("text")
// .attr("x", centroid[0] - w / 2)
// .attr("y", centroid[1] - h / 2)
// .text(function (d, i) {
// return name;
// });
});
// g.append("button")
// .attr("class", "wrap")
// .text((d) => d.properties.name);
}
);
function getCentroid(element) {
var bbox = element.getBBox();
return [bbox.x + bbox.width / 2, bbox.y + bbox.height / 2];
}
function clicked(d) {
var x, y, k;
if (d && centered !== d) {
// CENTROIDS for subregion provinces
var p = "";
json.features.forEach(function (f, i) {
if (f.properties.subregion === d.properties.subregion) {
p += path(f);
}
});
var tmp = svg.append("path");
tmp.attr("d", p);
var centroid = getCentroid(tmp.node());
tmp.remove();
// var centroid = path.centroid(p);
x = centroid[0];
y = centroid[1];
k = 2;
if (d.properties.subregion === "Northern Territories") {
k = 1.5;
}
centered = d;
} else {
x = width / 2;
y = height / 2;
k = 1;
centered = null;
}
g.selectAll("path").classed(
"active",
centered &&
function (d) {
return (
d.properties &&
d.properties.subregion === centered.properties.subregion
);
// return d === centered;
}
);
g.transition()
.duration(650)
.attr(
"transform",
"translate(" +
width / 2 +
"," +
height / 2 +
")scale(" +
k +
")translate(" +
-x +
"," +
-y +
")"
)
.style("stroke-width", 1.5 / k + "px");
}
.background-svg-map {
fill: none;
pointer-events: all;
}
#provinces {
fill: rgb(230, 230, 230);
}
#provinces > path:hover {
fill: #0630a6;
}
#provinces .active {
fill: #0630a6;
}
#province-borders-path {
fill: none;
stroke: #fff;
stroke-width: 1.5px;
stroke-linejoin: round;
stroke-linecap: round;
pointer-events: none;
}
<script src="https://d3js.org/d3.v3.min.js"></script>
<script src="https://d3js.org/topojson.v1.min.js"></script>
<div id="map-selector-app"></div>
The basic workflow for labels with a background is:
Position a parent g
Add the text
Add the rectangle and set it's size based on the bounding box of the parent g and then move it behind the text.
For d3v3, I'm going to actually add the rectangle before the text, but not style it until the text is added and the required size is known. If we append it after it will be infront of the text. In d3v4 there's a handy .lower() method that would move it backwards for us, in d3v3, there are ways to do this, but for simplicity, to ensure the rectangle is behind the text, I'll add it first
I'm going to deviate from your code at a more foundational level in my example. I'm not going to append child SVGs as this is introducing some sizing issues for you. Also, instead of using a loop to append the labels, I'm going to use a selectAll()/enter() cycle. This means I need a data array not an object ultimately. In order to help build that array I'll use an object though - by going through your json once, we can build a list of regions and create a geojson feature for each. The geojson feature is nice as it allows us to use path.centroid() which allows us to find the centroid of a feature without additional code.
So first, I need to create the data array:
var subregions = {};
json.features.forEach(function(feature) {
var subregion = feature.properties.subregion;
// Have we already encountered this subregion? If not, add it.
if(!(subregion in subregions)) {
subregions[subregion] = {"type":"FeatureCollection", features: [] };
}
// For every feature, add it to the subregion featureCollection:
subregions[subregion].features.push(feature);
})
// Convert to an array:
subregions = Object.keys(subregions).map(function(key) {
return { name: key, geojson: subregions[key] };
})
Now we can append the parent g with a standard d3 selectAll/enter statement:
// Create a parent g for each label:
var subregionsParent = g.selectAll(null)
.data(subregions)
.enter()
.append("g")
.attr("transform", function(d) {
// position the parent, so we don't need to position each child based on geographic location:
return "translate("+path.centroid(d.geojson)+")";
})
Now we can add the text and rectangle:
// add a rectangle to each parent `g`
var boxes = subregionsParent.append("rect");
// add text to each parent `g`
subregionsParent.append("text")
.text(function(d) { return d.name; })
.attr("text-anchor","middle");
// style the boxes based on the parent `g`'s bbox
boxes
.each(function() {
var bbox = this.parentNode.getBBox();
d3.select(this)
.attr("width", bbox.width + 10)
.attr("height", bbox.height +10)
.attr("x", bbox.x - 5)
.attr("y", bbox.y - 5)
.attr("rx", 10)
.attr("ry", 10)
.attr("fill","#ccc")
})
You can see the centroid method (whether using your existing function or path.centroid()) can be a little dumb when it comes to placement given some of the overlap on the map. There are ways you could modify that - perhaps adding offsets to the data, or manual exceptions when adding the text. Though on a larger SVG there shouldn't be overlap. Annotations are notoriously difficult to do.
Here's my result with the above:
And a snippet to demonstrate (I've removed a fair amount of unnecessary code to make a simpler example, but it should retain the functionality of your example):
var width = 500,
height = 275,
centered;
var projection = d3.geo.conicConformal()
.rotate([103, 0])
.center([0, 63])
.parallels([49, 77])
.scale(500)
.translate([width / 2.5, height / 2])
var path = d3.geo.path().projection(projection);
var svg = d3
.select("#map-selector-app")
.append("svg")
.attr("viewBox", `0 0 ${width} ${height}`);
var g = svg.append("g");
d3.json("https://gist.githubusercontent.com/KatFishSnake/7f3dc88b0a2fa0e8c806111f983dfa60/raw/7fff9e40932feb6c0181b8f3f983edbdc80bf748/canadaprovtopo.json",
function (error, canada) {
if (error) throw error;
json = topojson.feature(canada, canada.objects.canadaprov);
var provinces = g.append("g")
.attr("id", "provinces")
.selectAll("path")
.data(json.features)
.enter()
.append("path")
.attr("d", path)
.on("click", clicked);
g.append("g")
.attr("id", "province-borders")
.append("path")
.datum(topojson.mesh(canada, canada.objects.canadaprov, function (a,b) { return a !== b; }))
.attr("id", "province-borders-path")
.attr("d", path);
// Add labels:
// Get the data:
var subregions = {};
json.features.forEach(function(feature) {
var subregion = feature.properties.subregion;
if(!(subregion in subregions)) {
subregions[subregion] = {"type":"FeatureCollection", features: [] };
}
subregions[subregion].features.push(feature);
})
// Convert to an array:
subregions = Object.keys(subregions).map(function(key) {
return { name: key, geojson: subregions[key] };
})
// Create a parent g for each label:
var subregionsParent = g.selectAll(null)
.data(subregions)
.enter()
.append("g")
.attr("transform", function(d) {
return "translate("+path.centroid(d.geojson)+")";
})
var boxes = subregionsParent.append("rect");
subregionsParent.append("text")
.text(function(d) { return d.name; })
.attr("text-anchor","middle");
boxes
.each(function() {
var bbox = this.parentNode.getBBox();
d3.select(this)
.attr("width", bbox.width + 10)
.attr("height", bbox.height +10)
.attr("x", bbox.x - 5)
.attr("y", bbox.y - 5)
.attr("rx", 10)
.attr("ry", 10)
.attr("fill","#ccc")
})
// End labels.
function getCentroid(element) {
var bbox = element.getBBox();
return [bbox.x + bbox.width / 2, bbox.y + bbox.height / 2];
}
function clicked(d) {
var x, y, k;
if (d && centered !== d) {
// CENTROIDS for subregion provinces
var p = "";
json.features.forEach(function (f, i) {
if (f.properties.subregion === d.properties.subregion) {
p += path(f);
}
});
var tmp = svg.append("path");
tmp.attr("d", p);
var centroid = getCentroid(tmp.node());
tmp.remove();
x = centroid[0];
y = centroid[1];
k = 2;
if (d.properties.subregion === "Northern Territories") {
k = 1.5;
}
centered = d;
} else {
x = width / 2;
y = height / 2;
k = 1;
centered = null;
}
g.selectAll("path").classed(
"active",
centered &&
function (d) {
return (
d.properties &&
d.properties.subregion === centered.properties.subregion
);
}
);
g.transition()
.duration(650)
.attr("transform","translate(" + width / 2 + "," + height / 2 +")scale(" +
k +")translate(" + -x + "," + -y + ")"
)
.style("stroke-width", 1.5 / k + "px");
}
})
.background-svg-map {
fill: none;
pointer-events: all;
}
#provinces {
fill: rgb(230, 230, 230);
}
#provinces > path:hover {
fill: #0630a6;
}
#provinces .active {
fill: #0630a6;
}
#province-borders-path {
fill: none;
stroke: #fff;
stroke-width: 1.5px;
stroke-linejoin: round;
stroke-linecap: round;
pointer-events: none;
}
<script src="https://d3js.org/d3.v3.min.js"></script>
<script src="https://d3js.org/topojson.v1.min.js"></script>
<div id="map-selector-app"></div>
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>