CMDS
CMDS implements constrained MDS (Liu, Hu, North & Shen, Computer Graphics Forum 37(1), 2018). It alternates stress-majorization updates with rebuilding a Delaunay proximity graph, augmented with edges for overlapping pairs, until every item is inside the boundary with no overlap — then snaps the result to a grid.
The stress it descends is the usual weighted one, over the edges of that graph rather than over all pairs:
…
Drag the Round slider to scrub through the optimisation, or press Play to watch it run. The displacement histogram updates with the frame, so you can see the tail of far-flung points get pulled in — and see it stop improving.
It returns a generator
Section titled “It returns a generator”Alone among the methods, CMDS does not produce a final array. It yields each intermediate layout so you can animate the optimisation; the last yield is the grid-snapped result.
const { steps } = hagrid.gridify(data, "cmds");for (const [layout, proximity_graph, size] of steps) { positions = layout; pitch = size; // the node size in force, which changes as the grid grows}The generator is synchronous, so the loop above runs the whole optimisation in one blocking pass and freezes the page. Drive it a step at a time instead:
const step = () => { const { value, done } = steps.next(); if (done) return; draw(value[0], value[2]); requestAnimationFrame(step);};step();Parameters
Section titled “Parameters”| name | default | meaning |
|---|---|---|
Gamma | the data’s bounding square | boundary the layout is confined to, as a Bounds |
size | a cell of the m × n grid | starting node size; shrinks if the layout does not converge |
alpha | 0.1 | weight of the boundary constraint |
max_iter | 150 | rounds before the grid is grown |
max_rescales | 8 | how many times the grid may grow before giving up |
inner_iterations | 20 | inner iterations per round, in Algorithm 1 |
max_iter is the quality dial
Section titled “max_iter is the quality dial”Overlaps do fall at a fixed node size — 538 down to 24 over 1830 rounds in one measurement — so the layout largely resolves itself when given the chance. Growing the grid too eagerly instead crushes the nodes:
| rounds before growing | final packing |
|---|---|
| 10 | 2% |
| 200 | 33% |
| 800 | 71% |
71% is what the paper reports. Cost is linear in max_iter, so the default of 150 trades some
packing for responsiveness. Push the slider in the demo up and watch the cells get larger and
the run get slower.
The update rule
Section titled “The update rule”Each inner iteration moves every node to a weighted average of where its neighbours would like it to be, with the boundary pulling as one extra neighbour of weight :
Why the rescale loop is necessary
Section titled “Why the rescale loop is necessary”Without it the layout oscillates forever. The overlap correction enters the ideal distance as
where is the pair’s distance in the original layout and their current overlap. Once a pair is separated drops back to zero, the spring pulls them together again, and the overlap returns — measured overlap alternated exactly with period two.
The convergence machinery of §4.2 is what makes the update rule usable: the m × n grid that
sets the node size, inset by half a node so a node whose centre is
inside is drawn completely, the input uniformly scaled into , and growing
the grid when a run does not converge.
Output space
Section titled “Output space”The input’s own coordinate space, snapped so centres land on
Cell boundaries are therefore the multiples of size — which is what the demo’s grid is
anchored to.
References
Section titled “References”CorrelatedMultiples: Spatially Coherent Small Multiples With Constrained Multi-Dimensional Scaling Computer Graphics Forum 37(1), 7–18, 2018. doi:10.1111/cgf.12526
The constrained MDS this method implements. Section 4.2 is the convergence machinery described above; Algorithm 1 is the iteration.