diff --git a/__tests__/snapshots/d3-force/force-options.svg b/__tests__/snapshots/d3-force/force-options.svg index 7f6c3e1f..6aa96a6c 100644 --- a/__tests__/snapshots/d3-force/force-options.svg +++ b/__tests__/snapshots/d3-force/force-options.svg @@ -1,3670 +1,3670 @@ - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + diff --git a/__tests__/unit/d3-force.test.ts b/__tests__/unit/d3-force.test.ts index ac6cacea..937e2c3c 100644 --- a/__tests__/unit/d3-force.test.ts +++ b/__tests__/unit/d3-force.test.ts @@ -395,6 +395,88 @@ describe('layout d3-force', () => { await expect(canvas).toMatchSnapshot(__filename, 'force-options'); }); + describe('center position', () => { + const getCentroid = (layout: D3ForceLayout) => { + let x = 0; + let y = 0; + let count = 0; + layout.forEachNode((node) => { + x += node.x; + y += node.y; + count += 1; + }); + return [x / count, y / count]; + }; + + it('should use center.x and center.y', async () => { + const d3Force = new D3ForceLayout({ + width, + height, + center: { x: 300, y: 200 }, + }); + await d3Force.execute(data); + + const [x, y] = getCentroid(d3Force); + expect(x).toBeCloseTo(300, 0); + expect(y).toBeCloseTo(200, 0); + }); + + it('should use centerX and centerY', async () => { + const d3Force = new D3ForceLayout({ + width, + height, + centerX: 300, + centerY: 200, + }); + await d3Force.execute(data); + + const [x, y] = getCentroid(d3Force); + expect(x).toBeCloseTo(300, 0); + expect(y).toBeCloseTo(200, 0); + }); + + it('should use x.x and y.y', async () => { + const d3Force = new D3ForceLayout({ + width, + height, + // keep the center force enabled without moving nodes + center: { strength: 0 }, + x: { x: 100, strength: 1 }, + y: { y: 120, strength: 1 }, + }); + await d3Force.execute(data); + + const [x, y] = getCentroid(d3Force); + expect(x).toBeCloseTo(100, 0); + expect(y).toBeCloseTo(120, 0); + }); + + it('should use radial.x and radial.y', async () => { + const d3Force = new D3ForceLayout({ + width, + height, + center: { strength: 0 }, + manyBody: false, + link: false, + radial: { x: 100, y: 120, radius: 50, strength: 1 }, + }); + await d3Force.execute(data); + + d3Force.forEachNode((node) => { + expect(Math.hypot(node.x - 100, node.y - 120)).toBeCloseTo(50, 0); + }); + }); + + it('should fall back to the viewport center', async () => { + const d3Force = new D3ForceLayout({ width, height }); + await d3Force.execute(data); + + const [x, y] = getCentroid(d3Force); + expect(x).toBeCloseTo(width / 2, 0); + expect(y).toBeCloseTo(height / 2, 0); + }); + }); + it('should handle disabled forces', async () => { const d3Force = new D3ForceLayout({ width, diff --git a/src/algorithm/d3-force/index.ts b/src/algorithm/d3-force/index.ts index c2d2b197..b84b44ad 100644 --- a/src/algorithm/d3-force/index.ts +++ b/src/algorithm/d3-force/index.ts @@ -290,11 +290,16 @@ export class D3ForceLayout< width: options.width, height: options.height, }); - return assignDefined({}, options.center || {}, { - x: viewport.width / 2, - y: viewport.height / 2, - strength: options.centerStrength, - }) as T['center']; + return assignDefined( + {}, + { x: viewport.width / 2, y: viewport.height / 2 }, + options.center || {}, + { + x: options.centerX, + y: options.centerY, + strength: options.centerStrength, + }, + ) as T['center']; } protected setupCenterForce(simulation: Simulation, options: T) { @@ -447,8 +452,8 @@ export class D3ForceLayout< if (options.x === false) return undefined; const center = this.getCenterOptions(options); - return assignDefined({}, options.x || {}, { - x: options.forceXPosition ?? (center && center.x), + return assignDefined({}, { x: center && center.x }, options.x || {}, { + x: options.forceXPosition, strength: options.forceXStrength, }); } @@ -477,8 +482,8 @@ export class D3ForceLayout< if (options.y === false) return undefined; const center = this.getCenterOptions(options); - return assignDefined({}, options.y || {}, { - y: options.forceYPosition ?? (center && center.y), + return assignDefined({}, { y: center && center.y }, options.y || {}, { + y: options.forceYPosition, strength: options.forceYStrength, }); } @@ -511,12 +516,17 @@ export class D3ForceLayout< options.radialY !== undefined ) { const center = this.getCenterOptions(options); - return assignDefined({}, options.radial || {}, { - strength: options.radialStrength, - radius: options.radialRadius ?? 100, - x: options.radialX ?? (center && center.x), - y: options.radialY ?? (center && center.y), - }); + return assignDefined( + {}, + { radius: 100, x: center && center.x, y: center && center.y }, + options.radial || {}, + { + strength: options.radialStrength, + radius: options.radialRadius, + x: options.radialX, + y: options.radialY, + }, + ); } return undefined; }