NVD3 Scatter Chart: Bullet Stickers

I have a requirement that a bullet on a particular implementation of the scatter chart have labels next to them, however it is known that many of the datasets in the set are identical or very close to each other, so if I had to set the labels at a fixed coordinate relative to the bullet , tags will stack over each other and not be read.

I want to realize this so that the labels give way to each other - move, so they do not overlap - and I think this is a fairly common idea that some kind of approach already exists, but I have no idea what to look for. Does this concept have a name?

I would appreciate the implementation example, but this is not the most important thing. I am sure that I can solve this myself, but I would not invent something that someone else did better.

enter image description here

The above figure shows examples of a bullet on top and closer to each other

+4
source share
1 answer

In the end, I found inspiration in simulated annealing .

My solution looks like this:

/**
 * 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);
    }
}

There getScoreis room for improvement in the method , but the results are good enough for my case.

In principle, all points try to move to a random position in a given radius and see that this position is "better" than the original. The algorithm continues to do this for a smaller and smaller radius to radius = 0.

, , , .

+1

Source: https://habr.com/ru/post/1692750/


All Articles