英文:
How to properly arrange labels of a circle?
问题
我有一个如下所示的圆:
正如您可能注意到的,从0到9的标签与外圆的距离与其他标签不同。我的期望是:
- 如何使标签与外圆的距离相同?
- 如何安排标签,使它们位于从中心出发的线的中间,而不是在其侧面?
您可以在这里找到我的代码:https://jsfiddle.net/ao4xwpnk/
我尝试更改我的代码的这一部分,以使标签对称,但没有成功。
let featureData = features.map((f, i) => {
let angle = (Math.PI / 2) + (2 * Math.PI * i / features.length);
let isLeftHalf = angle < Math.PI; // 检查角度是否在圆的左半部分
let value = isLeftHalf ? 14.5 : 10.5; // 调整左右两侧标签的值
return {
"name": f,
"angle": angle,
"line_coord": angleToCoordinate(angle, 180),
"label_coord": angleToCoordinate(angle, value)
};
});
英文:
I have a circle like the following:
As you might notice, labels from 0 to 9 have different proximity to the outer circle in comparison to other labels. My expectations are:
- How can I have same distance of a label to the outer circle?
- How can I arrange labels so that they are in the middle of the line that comes from center, and not in the side of it?
You can find my code here: https://jsfiddle.net/ao4xwpnk/
I tried to change this part of my code to have symmetry of labels, but it did not work.
<!-- begin snippet: js hide: false console: true babel: false -->
<!-- language: lang-html -->
let featureData = features.map((f, i) => {
let angle = (Math.PI / 2) + (2 * Math.PI * i / features.length);
let isLeftHalf = angle < Math.PI; // Check if the angle is in the left half of the circle
let value = isLeftHalf ? 14.5 : 10.5; // Adjust the value for labels in left and right halves
return {
"name": f,
"angle": angle,
"line_coord": angleToCoordinate(angle, 180),
"label_coord": angleToCoordinate(angle, value)
};
});
<!-- end snippet -->
答案1
得分: 2
使用text
元素上的text-anchor
和dy
属性将有所帮助:
// 绘制轴标签
svg.selectAll(".axislabel")
.data(featureData)
.join(
enter => enter.append("text")
.attr("x", d => d.label_coord.x)
.attr("y", d => d.label_coord.y)
.attr("text-anchor", "middle") // <-- 在这里
.attr("dy", "0.35em") // <-- 以及在这里
.text(d => d.name)
)
text-anchor
用于水平定位:
渲染的字符被对齐,使得文本字符串的中间位于当前文本位置。
这解决了您尝试使用isLeftHalf
来解决的问题。
dy
属性用于垂直定位。值0.35em
是一个约定 - 可以参考这个答案,其中有趣地说:
使用
dy=0.35em
可以帮助在字体大小不同的情况下垂直居中文本。
我理解这个值考虑了字体设计中下行线的平均值,例如,对于字母"p"和"g",'尾巴'大致占据了高度的35%,因此您通过这个值将标签向下移动,以与坐标轴对齐。可以参考这里。
使用这个更新后的代码:
// 代码示例
希望这对您有所帮助。
英文:
Using text-anchor
and dy
attributes on the text
element will help:
// draw axis label
svg.selectAll(".axislabel")
.data(featureData)
.join(
enter => enter.append("text")
.attr("x", d => d.label_coord.x)
.attr("y", d => d.label_coord.y)
.attr("text-anchor", "middle") // <-- here
.attr("dy", "0.35em") // <-- and here
.text(d => d.name)
text-anchor
works for the horizontal placing:
> The rendered characters are aligned such that the middle of the text
> string is at the current text position.
Which solves the problem for you that you are trying to use isLeftHalf
for.
The dy
attribute works for the vertical placing. The value of 0.35em
is a convention - see this answer which interestingly says:
> Using dy=0.35em can help vertically centre text regardless of font
> size
My read on it is that this value accounts for an average of the descenders in the font design e.g. for p and g the 'tail' is roughly 35% of the height and you are therefore pushing the label 'down' by that to line it up with the axes. See here for example.
Your code with this update:
<!-- begin snippet: js hide: false console: true babel: false -->
<!-- language: lang-js -->
let data = [];
let features = [0, 23, 22, 21, 20, 19, 18, 17, 16, 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1]; // labels on the circle
//generate the data
for (var i = 0; i < 2; i++) {
var point = {}
//each feature will be a random number from 1-9
features.forEach(f => point[f] = 1 + Math.random() * 162);
data.push(point);
}
let width = 600;
let height = 600;
let svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height);
//adding radial scale
let radialScale = d3.scaleLinear()
.domain([0, 180])
.range([0, 250]);
//adding ticks
let ticks = [20, 40, 60, 80, 100, 120, 140, 160, 180]; // 9 circles
//adding circles
svg.selectAll("circle")
.data(ticks)
.join(
enter => enter.append("circle")
.attr("cx", width / 2)
.attr("cy", height / 2)
.attr("fill", "none")
.attr("stroke", "gray")
.attr("r", d => radialScale(d))
.attr("stroke-width", 1) // Set the same stroke-width for all circles
);
//adding white background circle to remove filling in the center
svg.append("circle")
.attr("cx", width / 2)
.attr("cy", height / 2)
.attr("fill", "white")
.attr("stroke", "gray")
.attr("r", radialScale(20))
.attr("stroke-width", 1); // Set the same stroke-width for the center circle
//adding text labels
svg.selectAll(".ticklabel")
.data(ticks)
.join(
enter => enter.append("text")
.attr("class", "ticklabel")
.attr("x", width / 2 + 5)
.attr("y", d => height / 2 - radialScale(d))
.text(d => d.toString())
);
//Plotting the Axes
//map an angle and value into SVG
function angleToCoordinate(angle, value) {
let x = Math.cos(angle) * radialScale(value);
let y = Math.sin(angle) * radialScale(value);
return { "x": width / 2 + x, "y": height / 2 - y };
}
let featureData = features.map((f, i) => {
let angle = (Math.PI / 2) + (2 * Math.PI * i / features.length);
let value = i < 9 ? 10.5 : 12; // Adjust the value for labels 13 to 21 to 12
return {
"name": f,
"angle": angle,
"line_coord": angleToCoordinate(angle, 180),
"label_coord": angleToCoordinate(angle, 190) // changin 190 allows me to adjust labels proximity to the circle!
};
});
// draw axis line
svg.selectAll("line")
.data(featureData)
.join(
enter => enter.append("line")
.attr("x1", width / 2)
.attr("y1", height / 2)
.attr("x2", d => d.line_coord.x)
.attr("y2", d => d.line_coord.y)
.attr("stroke", "black")
.attr("stroke-width", 1) // Set the same stroke-width for all axis lines
);
// draw axis label
svg.selectAll(".axislabel")
.data(featureData)
.join(
enter => enter.append("text")
.attr("x", d => d.label_coord.x)
.attr("y", d => d.label_coord.y)
.attr("text-anchor", "middle")
.attr("dy", "0.35em")
.text(d => d.name)
);
//draw shapes for actual data
let line = d3.line()
.x(d => d.x)
.y(d => d.y);
let area = d3.area()
.x(d => d.x)
.y0(d => d.y)
.y1(height / 2)
.curve(d3.curveLinear);
let colors = ["red", "green", "navy"];
//helper function to iterate through fields in each data point
function getPathCoordinates(data_point) {
let coordinates = [];
for (var i = 0; i < features.length; i++) {
let ft_name = features[i];
let angle = (Math.PI / 2) + (2 * Math.PI * i / features.length);
coordinates.push({
x: width / 2 + Math.cos(angle) * radialScale(data_point[ft_name]),
y: height / 2 - Math.sin(angle) * radialScale(data_point[ft_name])
});
}
return coordinates;
}
// Modify the path elements to separate red and green data
svg.selectAll(".path-red")
.data([data[0]]) // Data for the red line
.join(
enter => enter.append("path")
.datum(d => getPathCoordinates(d))
.attr("class", "path-red")
.attr("d", line)
.attr("stroke-width", 3)
.attr("stroke", "red")
.attr("fill", "none") // No fill for the red line
);
svg.selectAll(".path-green")
.data([data[1]]) // Data for the green line
.join(
enter => enter.append("path")
.datum(d => getPathCoordinates(d))
.attr("class", "path-green")
.attr("d", line)
.attr("stroke-width", 3)
.attr("stroke", "green")
.attr("fill", "none") // No fill for the green line
);
// Fill the area between the red and green lines
let areaFillData = [
...getPathCoordinates(data[0]),
...getPathCoordinates(data[1]).reverse()
];
svg.append("path")
.datum(areaFillData)
.attr("d", area)
.attr("stroke-width", 0)
.attr("fill", "blue") // Fill the area between the red and green lines with blue color
.attr("fill-opacity", 0.3); // You can adjust the opacity as you like
<!-- language: lang-html -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/7.8.0/d3.min.js"></script>
<!-- end snippet -->
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论