|
| 1 | +import {max} from "d3"; |
| 2 | +import IntervalTree from "interval-tree-1d"; |
| 3 | +import {maybeNumberChannel} from "../options.js"; |
| 4 | + |
| 5 | +const anchorXLeft = ({marginLeft}) => [1, marginLeft]; |
| 6 | +const anchorXRight = ({width, marginRight}) => [-1, width - marginRight]; |
| 7 | +const anchorXMiddle = ({width, marginLeft, marginRight}) => [0, (marginLeft + width - marginRight) / 2]; |
| 8 | +const anchorYTop = ({marginTop}) => [1, marginTop]; |
| 9 | +const anchorYBottom = ({height, marginBottom}) => [-1, height - marginBottom]; |
| 10 | +const anchorYMiddle = ({height, marginTop, marginBottom}) => [0, (marginTop + height - marginBottom) / 2]; |
| 11 | + |
| 12 | +function maybeDodge(options) { |
| 13 | + return typeof options === "string" ? {anchor: options} : options; |
| 14 | +} |
| 15 | + |
| 16 | +export function dodgeX(dodgeOptions = {}, options = {}) { |
| 17 | + if (arguments.length === 1) [options, dodgeOptions] = [dodgeOptions, options]; |
| 18 | + let {anchor = "left", padding} = maybeDodge(dodgeOptions); |
| 19 | + switch (`${anchor}`.toLowerCase()) { |
| 20 | + case "left": anchor = anchorXLeft; break; |
| 21 | + case "right": anchor = anchorXRight; break; |
| 22 | + case "middle": anchor = anchorXMiddle; break; |
| 23 | + default: throw new Error(`unknown dodge anchor: ${anchor}`); |
| 24 | + } |
| 25 | + return dodge("x", "y", anchor, padding, options); |
| 26 | +} |
| 27 | + |
| 28 | +export function dodgeY(dodgeOptions = {}, options = {}) { |
| 29 | + if (arguments.length === 1) [options, dodgeOptions] = [dodgeOptions, options]; |
| 30 | + let {anchor = "bottom", padding} = maybeDodge(dodgeOptions); |
| 31 | + switch (`${anchor}`.toLowerCase()) { |
| 32 | + case "top": anchor = anchorYTop; break; |
| 33 | + case "bottom": anchor = anchorYBottom; break; |
| 34 | + case "middle": anchor = anchorYMiddle; break; |
| 35 | + default: throw new Error(`unknown dodge anchor: ${anchor}`); |
| 36 | + } |
| 37 | + return dodge("y", "x", anchor, padding, options); |
| 38 | +} |
| 39 | + |
| 40 | +function dodge(y, x, anchor, padding = 1, options) { |
| 41 | + const [, r] = maybeNumberChannel(options.r, 3); |
| 42 | + if (options[x] == null) throw new Error(`missing channel: ${x}`); |
| 43 | + return { |
| 44 | + ...options, |
| 45 | + layout(I, scales, values, dimensions) { // TODO wrap previous layout? |
| 46 | + let {[x]: X, r: R} = values; |
| 47 | + let [ky, ty] = anchor(dimensions); |
| 48 | + const compare = ky ? compareAscending : compareSymmetric; |
| 49 | + if (ky) ty += ky * ((R ? max(I, i => R[i]) : r) + padding); else ky = 1; |
| 50 | + if (!R) R = new Float64Array(X.length).fill(r); |
| 51 | + const Y = new Float64Array(X.length); |
| 52 | + const tree = IntervalTree(); |
| 53 | + for (const i of I) { |
| 54 | + const intervals = []; |
| 55 | + const l = X[i] - R[i]; |
| 56 | + const r = X[i] + R[i]; |
| 57 | + |
| 58 | + // For any previously placed circles that may overlap this circle, compute |
| 59 | + // the y-positions that place this circle tangent to these other circles. |
| 60 | + // https://observablehq.com/@mbostock/circle-offset-along-line |
| 61 | + tree.queryInterval(l - padding, r + padding, ([,, j]) => { |
| 62 | + const yj = Y[j]; |
| 63 | + const dx = X[i] - X[j]; |
| 64 | + const dr = R[i] + padding + R[j]; |
| 65 | + const dy = Math.sqrt(dr * dr - dx * dx); |
| 66 | + intervals.push([yj - dy, yj + dy]); |
| 67 | + }); |
| 68 | + |
| 69 | + // Find the best y-value where this circle can fit. |
| 70 | + for (let y of intervals.flat().sort(compare)) { |
| 71 | + if (intervals.every(([lo, hi]) => y <= lo || y >= hi)) { |
| 72 | + Y[i] = y; |
| 73 | + break; |
| 74 | + } |
| 75 | + } |
| 76 | + |
| 77 | + // Insert the placed circle into the interval tree. |
| 78 | + tree.insert([l, r, i]); |
| 79 | + } |
| 80 | + return {...values, [y]: Y.map(y => y * ky + ty)}; |
| 81 | + } |
| 82 | + }; |
| 83 | +} |
| 84 | + |
| 85 | +function compareSymmetric(a, b) { |
| 86 | + return Math.abs(a) - Math.abs(b); |
| 87 | +} |
| 88 | + |
| 89 | +function compareAscending(a, b) { |
| 90 | + return (a < 0) - (b < 0) || (a - b); |
| 91 | +} |
0 commit comments