nvd3散点图:标签在子弹上

4
我有一个需求,需要在散点图的特定实现中,弹出框旁边添加标签,但是已知数据集中许多数据点是相同或非常接近的,因此如果我将标签设置在固定坐标上,则标签会重叠在一起而无法阅读。
我想要实现这样的效果,使标签互相让路 - 移动位置,以避免重叠。我认为这是一个很常见的想法,可能已经存在某种方法,但是我不知道该搜索什么。这个概念有名称吗?
当然,我会感激一个实现示例,但那并不是最重要的事情。我相信我可以自己解决它,但我不想重新发明已经被其他人更好地完成的事情。

enter image description here

上面的图片显示了彼此靠近并在顶部的子弹点的示例。

实际上,在散点增多时,在散点周围放置标签并不是一个好主意,因为文字和散点重叠会让事情变得混乱。我更喜欢使用工具提示。 - Help
1个回答

1

我最终在模拟退火算法中找到了灵感。

我的解决方案看起来像这样

/**
 * 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方法还有改进的空间,但结果已经足够好了。

基本上,所有点都尝试移动到给定半径内的随机位置,并查看该位置是否比原始位置更好。该算法会不断在更小的半径下执行此操作,直到半径为0。

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


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接