Plot nvd3散点图:项目符号上的标签

Plot nvd3散点图:项目符号上的标签,plot,nvd3.js,Plot,Nvd3.js,我有一个要求,即散点图的特定实现上的项目符号需要在其旁边有标签,但是,已知集合中的许多数据点是相同的或彼此非常接近的,因此如果我要在相对于项目符号的固定坐标上设置标签,标签会堆叠在一起,不可读 我想实现这一点,这样标签就会相互让位——四处移动,这样它们就不会重叠——我认为这是一个足够普遍的想法,有些方法已经存在,但我不知道要搜索什么。这个概念有名字吗 当然,我希望有一个实现示例,但这不是最重要的。我相信我自己可以解决这个问题,但我不想重新发明别人已经做得更好的东西 上图显示了子弹相互重叠和靠近

我有一个要求,即散点图的特定实现上的项目符号需要在其旁边有标签,但是,已知集合中的许多数据点是相同的或彼此非常接近的,因此如果我要在相对于项目符号的固定坐标上设置标签,标签会堆叠在一起,不可读

我想实现这一点,这样标签就会相互让位——四处移动,这样它们就不会重叠——我认为这是一个足够普遍的想法,有些方法已经存在,但我不知道要搜索什么。这个概念有名字吗

当然,我希望有一个实现示例,但这不是最重要的。我相信我自己可以解决这个问题,但我不想重新发明别人已经做得更好的东西


上图显示了子弹相互重叠和靠近的例子

我最终从中找到了灵感

我的解决方案是这样的

/**
 * 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);
    }
}
/**
*实现在图表上放置标签的算法,以便
*不要重叠太多。
*该方法受到模拟退火的启发
* (https://en.wikipedia.org/wiki/Simulated_annealing)
*/
出口级砂矿{
私有已知位置:坐标[];
专用起点半径=20;
私人跑步=15;
私人来源_权重=2;
构造函数(){
this.knownPositions=[]
}
/**
*找到一个放置对象的好位置。
*
*给定一个起始坐标,该方法试图找到最佳位置
*这接近于该点,但不太接近其他已知点。
*
*@param{Coordinate}Coordinate
*@returns{Coordinate}
*/
getPlacement(坐标:坐标):坐标{
设半径=此。开始半径;
让lastPosition=坐标;
设lastScore=0;
同时(半径>0){
const newPosition=this.getRandomPosition(坐标、半径);
const newScore=this.getScore(newPosition,坐标);
如果(新闻核心>最新分数){
lastPosition=新位置;
lastScore=新闻核心;
}
半径-=this.START\u半径/this.RUNS;
}
此.known位置.push(最后位置);
返回最后位置;
}
/**
*返回位置周围半径上的随机点
*
*@param{Coordinate}位置中心点
*@param{number}半径从“位置”到找到点的距离
*@returns{Coordinate}一个随机点`半径'距离
*“位置`
*/
私有getRandomPosition(位置:坐标,半径:编号):坐标{
常量随机旋转=弧度(Math.random()*360);
常数xOffset=数学cos(随机旋转)*半径;
常数yOffset=数学sin(随机旋转)*半径;
返回{
x:位置x+x偏移,
y:位置,y+y偏移,
}
}
/**
*返回一个位置的数字分数。它离任何位置越远
*其他已知点,分数越高(数字越大),但是,它
*离原点越远,分数就会减少
*重点。
*
*@param{Coordinate}定位要得分的位置
*@param{Coordinate}在查找之前将初始位置作为原点
*更好的
*@返回{number}分数的表示形式
*/
私有getScore(位置:坐标,原点:坐标):编号{
让最近的:number=null;
this.knownPositions.forEach((knownPosition)=>{
常量距离=Math.abs(Math.sqrt(
数学.pow(knownPosition.x-position.x,2)+
数学.pow(knownPosition.y-position.y,2)
));
如果(最近===null | |距离<最近){
最近距离=距离;
}
});
const distance toorigin=Math.abs(Math.sqrt(
Math.pow(origin.x-position.x,2)+
数学.pow(origin.y-position.y,2)
));
返回最近的-(距离原点/此原点\重量);
}
}
getScore
方法还有改进的余地,但是对于我的案例来说,结果已经足够好了

基本上,所有点都尝试移动到给定半径内的随机位置,并查看该位置是否比原始位置“更好”。该算法对越来越小的半径一直这样做,直到半径=0


该类跟踪所有已知点,因此当您尝试放置第二点时,评分可以解释第一点的存在。

实际上,在分散点周围添加标签不是一个好主意,因为点增加了文本的重叠,点会使事情变得笨拙。我更喜欢有一个工具提示。