src/lib/shared/helpers/labelsUtil.js (69 lines of code) (raw):

// @ts-check /** * @template {Record<string, unknown>} T * @param {T[]} labelPositions * @param {number} iteration * @param {number} labelSize * @param {string} coordinate * @param {boolean} moveBothLabels * @returns */ export function preventOverlap( labelPositions, iteration = 0, labelSize = 12, coordinate = "y", moveBothLabels = true, ) { const maxIterations = 10 let totalOverlap = 0 if (!isArrayWithCoordinates(labelPositions, coordinate)) return labelPositions for (let index = 1; index < labelPositions.length; index++) { const previousElement = labelPositions[index - 1] const element = labelPositions[index] const overlap = previousElement[coordinate] - (element[coordinate] - labelSize) if (overlap < 0) { // no overlap, continue continue } if (moveBothLabels) { // @ts-ignore previousElement[coordinate] -= overlap / 2 // @ts-ignore element[coordinate] += overlap / 2 } else { // @ts-ignore previousElement[coordinate] -= overlap } totalOverlap += overlap } if (totalOverlap > 0 && iteration < maxIterations) { return preventOverlap( labelPositions, iteration + 1, labelSize, coordinate, moveBothLabels, ) } return labelPositions } /** * Unique objects for a given key’s value; * * @example * ``` * const labels = [ * {key: 'alpha', value: 1 }, * {key: 'alpha', value: 9 }, * {key: 'delta', value: 4 }, *]; * console.log(uniqueBy(labels, 'key')); * // [ * // {key: 'alpha', value: 1 }, * // {key: 'delta', value: 4 }, * // ] * ``` * * @template T extends Record<string, unknown> * @param {T[]} array * @param {keyof T} key * @returns {T[]} */ export function uniqueBy(array, key) { return [...array.reduce((map, d) => map.set(d[key], d), new Map()).values()] } /** * @template {{ value: string }} T * @param {T[]} labels * @param {number} labelSize * @param {string} coordinate * @param {boolean} moveBothLabels * @returns {T[]} */ export function positionLabels( labels, labelSize = 12, coordinate = "y", moveBothLabels = true, ) { labels = uniqueBy(labels, "value") // sort by coordinate-position labels.sort((a, b) => a[coordinate] - b[coordinate]) return preventOverlap(labels, 0, labelSize, coordinate, moveBothLabels) } /** * Create a function that maps a value from a source domain to a target range. * * same as this one https://gist.github.com/vectorsize/7031902 * * @param {[number, number]} domain * @param {[number, number]} range * @returns {(x: number) => number} */ export function scaleLinear(domain, range) { const [domainMin, domainMax] = domain const [rangeMin, rangeMax] = range const slope = (rangeMax - rangeMin) / (domainMax - domainMin) const intercept = rangeMin - slope * domainMin return function (x) { return slope * x + intercept } } /** * Ensures that all items in the array have a coordinate of type `number` * * @template {Record<string, unknown>} T * @template {string} K * @param {T[]} labelPositions * @param {K} coordinate * @returns {labelPositions is Array<T & { [key in K]: number }>} */ function isArrayWithCoordinates(labelPositions, coordinate) { return labelPositions.every( (position) => typeof position[coordinate] === "number", ) }