UNPKG

@tanstack/charts

Version:

A chart grammar for TypeScript and JavaScript. Marks consume your data directly, channels describe visual encodings, and the engine compiles them into a renderer-neutral keyed scene. TanStack's compact scales cover common numeric and categorical mappings.

364 lines (363 loc) 11 kB
import { forceCenter, forceCollide, forceLink, forceManyBody, forceSimulation, forceX, forceY } from "d3-force"; import { resolveNetworkGraph } from "./network-graph-internal.js"; import { transformValues } from "./transform-internal.js"; function forceLayout(nodes, links, options) { const graph = resolveNetworkGraph( nodes, links, { nodeKey: options.nodeKey, source: options.source, target: options.target }, "forceLayout" ); const nodeData = graph.nodes; const linkData = graph.links; const iterations = options.iterations ?? 300; const domainPadding = options.domainPadding ?? 0.2; assertNonnegativeInteger(iterations, "iterations"); assertNonnegativeFinite(domainPadding, "domainPadding"); assertUniqueForces(options.forces); const { nodeKeys, sourceKeys, targetKeys, nodeIndexes } = graph; const workingNodes = nodeData.map(createWorkingNode); const workingLinks = linkData.map( (link, index) => createWorkingLink( link, sourceKeys[index], targetKeys[index] ) ); const originalWorkingNodes = [...workingNodes]; const originalWorkingLinks = [...workingLinks]; const factoryContext = createForceFactoryContext( workingNodes, workingLinks, nodeKeys, sourceKeys, targetKeys ); const preparedForces = options.forces.map((descriptor, index) => ({ descriptor, force: createForce( descriptor, index, nodeData, linkData, nodeKeys, workingLinks, factoryContext ) })); assertWorkingCollection(workingNodes, originalWorkingNodes, "node"); assertWorkingCollection(workingLinks, originalWorkingLinks, "link"); const simulation = forceSimulation(workingNodes).stop(); preparedForces.forEach(({ descriptor, force }, index) => { simulation.force(`${index}:${forceName(descriptor)}`, force); }); simulation.tick(iterations); assertWorkingCollection(workingNodes, originalWorkingNodes, "node"); assertWorkingCollection(workingLinks, originalWorkingLinks, "link"); const outputNodes = workingNodes.map((node, index) => { const datum = nodeData[index]; return { ...datum, x: coordinate(node.x, index, "x"), y: coordinate(node.y, index, "y"), vx: coordinate(node.vx, index, "vx"), vy: coordinate(node.vy, index, "vy"), source: [datum], sourceIndexes: [index] }; }); const outputLinks = linkData.map((link, index) => { const source = sourceKeys[index]; const target = targetKeys[index]; const sourceIndex = nodeIndexes.get(source); const targetIndex = nodeIndexes.get(target); const sourceNode = outputNodes[sourceIndex]; const targetNode = outputNodes[targetIndex]; return { ...link, source, target, sourceKey: source, targetKey: target, sourceIndex, targetIndex, sourceNode, targetNode, x1: sourceNode.x, y1: sourceNode.y, x2: targetNode.x, y2: targetNode.y, sourceRows: [link], sourceIndexes: [index] }; }); return { nodes: outputNodes, links: outputLinks, xDomain: paddedDomain( outputNodes.map((node) => node.x), domainPadding ), yDomain: paddedDomain( outputNodes.map((node) => node.y), domainPadding ) }; } const simulationNodeFields = ["x", "y", "vx", "vy", "fx", "fy"]; function createWorkingNode(node) { const working = { ...node }; delete working.index; for (const field of simulationNodeFields) { const value = working[field]; const validFixedValue = (field === "fx" || field === "fy") && value === null; if (value !== void 0 && !validFixedValue && (typeof value !== "number" || !Number.isFinite(value))) { delete working[field]; } } return working; } function createWorkingLink(link, source, target) { const working = { ...link, source, target }; delete working.index; return working; } function createForce(descriptor, descriptorIndex, nodes, links, nodeKeys, workingLinks, factoryContext) { const name = `forces[${descriptorIndex}] (${forceName(descriptor)})`; switch (descriptor.type) { case "link": { const force = forceLink( workingLinks ).id((_node, index) => nodeKeys[index]); const distance = forceValue( links, descriptor.distance, `${name}.distance`, assertNonnegativeFinite ); const strength = forceValue( links, descriptor.strength, `${name}.strength`, assertNonnegativeFinite ); if (distance !== void 0) force.distance(distance); if (strength !== void 0) force.strength(strength); return force; } case "manyBody": { const force = forceManyBody(); const strength = forceValue( nodes, descriptor.strength, `${name}.strength` ); if (strength !== void 0) force.strength(strength); return force; } case "center": assertOptionalFinite(descriptor.x, `${name}.x`); assertOptionalFinite(descriptor.y, `${name}.y`); return forceCenter(descriptor.x, descriptor.y); case "collide": { const force = forceCollide(); const radius = forceValue( nodes, descriptor.radius, `${name}.radius`, assertNonnegativeFinite ); if (radius !== void 0) force.radius(radius); if (descriptor.strength !== void 0) { assertRange(descriptor.strength, 0, 1, `${name}.strength`); force.strength(descriptor.strength); } return force; } case "x": { const force = forceX(); const x = forceValue(nodes, descriptor.x, `${name}.x`); const strength = forceValue( nodes, descriptor.strength, `${name}.strength`, assertUnitInterval ); if (x !== void 0) force.x(x); if (strength !== void 0) force.strength(strength); return force; } case "y": { const force = forceY(); const y = forceValue(nodes, descriptor.y, `${name}.y`); const strength = forceValue( nodes, descriptor.strength, `${name}.strength`, assertUnitInterval ); if (y !== void 0) force.y(y); if (strength !== void 0) force.strength(strength); return force; } case "custom": { const force = descriptor.create(factoryContext); if (typeof force !== "function") { throw new TypeError( `forceLayout: ${name}.create must return a D3-compatible force` ); } return force; } } } function forceValue(data, value, name, validate = assertFinite) { if (value === void 0) return void 0; if (typeof value === "number") { validate(value, name); return value; } const values = transformValues(data, value); values.forEach((resolved, index) => { assertFinite(resolved, `${name} at index ${index}`); validate(resolved, `${name} at index ${index}`); }); return (_datum, index) => values[index]; } function assertUniqueForces(descriptors) { const types = /* @__PURE__ */ new Set(); const names = /* @__PURE__ */ new Set(); descriptors.forEach((descriptor, index) => { const type = descriptor.type; if (type === "custom") { const custom = descriptor; if (typeof custom.name !== "string" || !custom.name.trim()) { throw new TypeError( `forceLayout: forces[${index}].name must be a nonempty string` ); } if (typeof custom.create !== "function") { throw new TypeError( `forceLayout: forces[${index}] (${custom.name}).create must be a function` ); } if (names.has(custom.name)) { throw new TypeError( `forceLayout: duplicate force name "${custom.name}"` ); } names.add(custom.name); return; } if (type !== "link" && type !== "manyBody" && type !== "center" && type !== "collide" && type !== "x" && type !== "y") { throw new TypeError(`forceLayout: forces[${index}] has an unknown type`); } if (types.has(type)) { throw new TypeError(`forceLayout: duplicate force type "${type}"`); } if (names.has(type)) { throw new TypeError(`forceLayout: duplicate force name "${type}"`); } types.add(type); names.add(type); }); } function forceName(descriptor) { return descriptor.type === "custom" ? descriptor.name : descriptor.type; } function createForceFactoryContext(nodes, links, nodeKeys, sourceKeys, targetKeys) { const keyByNode = new Map( nodes.map((node, index) => [node, nodeKeys[index]]) ); return Object.freeze({ nodes, links, nodeKeys: Object.freeze([...nodeKeys]), sourceKeys: Object.freeze([...sourceKeys]), targetKeys: Object.freeze([...targetKeys]), nodeKey: (node) => { const key = keyByNode.get(node); if (key === void 0) { throw new TypeError( "forceLayout: custom force requested the key of a foreign node" ); } return key; } }); } function assertWorkingCollection(values, expected, name) { if (values.length !== expected.length || values.some((value, index) => value !== expected[index])) { throw new TypeError( `forceLayout: custom force changed the private ${name} collection` ); } } function assertNonnegativeInteger(value, name) { if (!Number.isInteger(value) || value < 0) { throw new TypeError(`forceLayout: ${name} must be a nonnegative integer`); } } function assertNonnegativeFinite(value, name) { if (!Number.isFinite(value) || value < 0) { throw new TypeError( `forceLayout: ${name} must be a nonnegative finite number` ); } } function assertUnitInterval(value, name) { assertRange(value, 0, 1, name); } function assertOptionalFinite(value, name) { if (value !== void 0) assertFinite(value, name); } function assertRange(value, minimum, maximum, name) { if (!Number.isFinite(value) || value < minimum || value > maximum) { throw new TypeError( `forceLayout: ${name} must be between ${minimum} and ${maximum}` ); } } function assertFinite(value, name) { if (typeof value !== "number" || !Number.isFinite(value)) { throw new TypeError(`forceLayout: ${name} must be a finite number`); } } function coordinate(value, index, coordinateName) { if (value === void 0 || !Number.isFinite(value)) { throw new TypeError( `forceLayout: simulation produced a non-finite ${coordinateName} for node index ${index}` ); } return value; } function paddedDomain(values, padding) { if (values.length === 0) return [-1, 1]; let minimum = values[0]; let maximum = minimum; for (const value of values.slice(1)) { if (value < minimum) minimum = value; if (value > maximum) maximum = value; } const amount = Math.max(1, maximum - minimum) * padding; return [minimum - amount, maximum + amount]; } export { forceLayout };