nvd3 散点图:项目符号上的标签
nvd3 scatterplot: labels on bullets
我有一个要求,散点图的特定实现上的项目符号旁边需要有标签,但是,众所周知,集合中的许多数据点是相同的或彼此非常接近,所以如果我要在相对于子弹的固定坐标上设置标签,标签将堆叠在彼此的顶部并且不可读。
我想实现这个,这样标签就会互相让路——四处移动,所以它们不会重叠——我认为这是一个足够普遍的想法,一些方法已经存在,但我有不知道要搜索什么。这个概念有名字吗?
我当然希望有一个实施示例,但这不是最重要的事情。我确信我可以自己解决它,但我宁愿不重新发明别人已经做得更好的东西。
上图显示了子弹相互重叠和靠近的示例
我最终在 Simulated Annealing 中找到了灵感。
我的解决方案是这样的
/**
* Implements an algorithm for placing labels on a chart in a way so that they
* do not overlap as much.
* The approach is inspired by Simulated Annealing
* (https://en.wikipedia.org/wiki/Simulated_annealing)
*/
export class Placer {
private knownPositions: Coordinate[];
private START_RADIUS = 20;
private RUNS = 15;
private ORIGIN_WEIGHT = 2;
constructor() {
this.knownPositions = []
}
/**
* Get a good spot to place the object.
*
* Given a start coordinate, this method tries to find the best place
* that is close to that point but not too close to other known points.
*
* @param {Coordinate} coordinate
* @returns {Coordinate}
*/
getPlacement(coordinate: Coordinate) : Coordinate {
let radius = this.START_RADIUS;
let lastPosition = coordinate;
let lastScore = 0;
while (radius > 0) {
const newPosition = this.getRandomPosition(coordinate, radius);
const newScore = this.getScore(newPosition, coordinate);
if (newScore > lastScore) {
lastPosition = newPosition;
lastScore = newScore;
}
radius -= this.START_RADIUS / this.RUNS;
}
this.knownPositions.push(lastPosition);
return lastPosition;
}
/**
* Return a random point on the radius around the position
*
* @param {Coordinate} position Center point
* @param {number} radius Distance from `position` to find a point
* @returns {Coordinate} A random point `radius` distance away from
* `position`
*/
private getRandomPosition(position: Coordinate, radius:number) : Coordinate {
const randomRotation = radians(Math.random() * 360);
const xOffset = Math.cos(randomRotation) * radius;
const yOffset = Math.sin(randomRotation) * radius;
return {
x: position.x + xOffset,
y: position.y + yOffset,
}
}
/**
* Returns a number score of a position. The further away it is from any
* other known point, the better the score (bigger number), however, it
* suffers a subtraction in score the further away it gets from its origin
* point.
*
* @param {Coordinate} position The position to score
* @param {Coordinate} origin The initial position before looking for
* better ones
* @returns {number} The representation of the score
*/
private getScore(position: Coordinate, origin: Coordinate) : number {
let closest: number = null;
this.knownPositions.forEach((knownPosition) => {
const distance = Math.abs(Math.sqrt(
Math.pow(knownPosition.x - position.x, 2) +
Math.pow(knownPosition.y - position.y, 2)
));
if (closest === null || distance < closest) {
closest = distance;
}
});
const distancetoOrigin = Math.abs(Math.sqrt(
Math.pow(origin.x - position.x, 2) +
Math.pow(origin.y - position.y, 2)
));
return closest - (distancetoOrigin / this.ORIGIN_WEIGHT);
}
}
getScore
方法还有改进的余地,但结果对我来说已经足够好了。
基本上,所有点都尝试移动到给定半径内的随机位置,并查看该位置是否比原始位置 "better"。该算法一直在为越来越小的半径执行此操作,直到半径 = 0。
class 跟踪所有已知点,因此当您尝试放置第二个点时,得分可以说明第一个点的存在。
我有一个要求,散点图的特定实现上的项目符号旁边需要有标签,但是,众所周知,集合中的许多数据点是相同的或彼此非常接近,所以如果我要在相对于子弹的固定坐标上设置标签,标签将堆叠在彼此的顶部并且不可读。
我想实现这个,这样标签就会互相让路——四处移动,所以它们不会重叠——我认为这是一个足够普遍的想法,一些方法已经存在,但我有不知道要搜索什么。这个概念有名字吗?
我当然希望有一个实施示例,但这不是最重要的事情。我确信我可以自己解决它,但我宁愿不重新发明别人已经做得更好的东西。
上图显示了子弹相互重叠和靠近的示例
我最终在 Simulated Annealing 中找到了灵感。
我的解决方案是这样的
/**
* Implements an algorithm for placing labels on a chart in a way so that they
* do not overlap as much.
* The approach is inspired by Simulated Annealing
* (https://en.wikipedia.org/wiki/Simulated_annealing)
*/
export class Placer {
private knownPositions: Coordinate[];
private START_RADIUS = 20;
private RUNS = 15;
private ORIGIN_WEIGHT = 2;
constructor() {
this.knownPositions = []
}
/**
* Get a good spot to place the object.
*
* Given a start coordinate, this method tries to find the best place
* that is close to that point but not too close to other known points.
*
* @param {Coordinate} coordinate
* @returns {Coordinate}
*/
getPlacement(coordinate: Coordinate) : Coordinate {
let radius = this.START_RADIUS;
let lastPosition = coordinate;
let lastScore = 0;
while (radius > 0) {
const newPosition = this.getRandomPosition(coordinate, radius);
const newScore = this.getScore(newPosition, coordinate);
if (newScore > lastScore) {
lastPosition = newPosition;
lastScore = newScore;
}
radius -= this.START_RADIUS / this.RUNS;
}
this.knownPositions.push(lastPosition);
return lastPosition;
}
/**
* Return a random point on the radius around the position
*
* @param {Coordinate} position Center point
* @param {number} radius Distance from `position` to find a point
* @returns {Coordinate} A random point `radius` distance away from
* `position`
*/
private getRandomPosition(position: Coordinate, radius:number) : Coordinate {
const randomRotation = radians(Math.random() * 360);
const xOffset = Math.cos(randomRotation) * radius;
const yOffset = Math.sin(randomRotation) * radius;
return {
x: position.x + xOffset,
y: position.y + yOffset,
}
}
/**
* Returns a number score of a position. The further away it is from any
* other known point, the better the score (bigger number), however, it
* suffers a subtraction in score the further away it gets from its origin
* point.
*
* @param {Coordinate} position The position to score
* @param {Coordinate} origin The initial position before looking for
* better ones
* @returns {number} The representation of the score
*/
private getScore(position: Coordinate, origin: Coordinate) : number {
let closest: number = null;
this.knownPositions.forEach((knownPosition) => {
const distance = Math.abs(Math.sqrt(
Math.pow(knownPosition.x - position.x, 2) +
Math.pow(knownPosition.y - position.y, 2)
));
if (closest === null || distance < closest) {
closest = distance;
}
});
const distancetoOrigin = Math.abs(Math.sqrt(
Math.pow(origin.x - position.x, 2) +
Math.pow(origin.y - position.y, 2)
));
return closest - (distancetoOrigin / this.ORIGIN_WEIGHT);
}
}
getScore
方法还有改进的余地,但结果对我来说已经足够好了。
基本上,所有点都尝试移动到给定半径内的随机位置,并查看该位置是否比原始位置 "better"。该算法一直在为越来越小的半径执行此操作,直到半径 = 0。
class 跟踪所有已知点,因此当您尝试放置第二个点时,得分可以说明第一个点的存在。