{"id":"a6c00de7c09bdfa1","slug":"force-directed-graph-component","first_public_version":327,"paused_version":null,"likes":417,"publish_level":"live","forks":922,"fork_of":null,"has_importers":true,"thumbnail":"93916ad134bba33410c07400a5dc2c8474d9fe95fdf5c5c6705705f8093a3dbe","default_thumbnail":"93916ad134bba33410c07400a5dc2c8474d9fe95fdf5c5c6705705f8093a3dbe","update_time":"2020-09-01T21:04:45.490Z","publish_time":"2023-09-27T12:51:27.464Z","publish_version":340,"latest_version":340,"roles":[],"sharing":null,"owner":{"id":"863951e3ebe4c0ae","avatar_url":"https://avatars.observableusercontent.com/avatar/5af16e327a90b2873351dda8a596c0d2d3bf954f64523deefe80177c9764d0f7","login":"d3","name":"D3","bio":"Bring your data to life.","home_url":"https://d3js.org","type":"team","tier":"pro_2024"},"creator":{"id":"074c414ad1d825f5","avatar_url":"https://avatars.observableusercontent.com/avatar/82811927da99f8938001b2ef1f552ad2c47083e46ebc55a3a146a5a5848c4519","login":"mbostock","name":"Mike Bostock","bio":"Visualization toolmaker. Founder @observablehq. Creator @d3. Former @nytgraphics. Pronounced BOSS-tock.","home_url":"https://bost.ocks.org/mike/","tier":"pro"},"authors":[{"id":"074c414ad1d825f5","avatar_url":"https://avatars.observableusercontent.com/avatar/82811927da99f8938001b2ef1f552ad2c47083e46ebc55a3a146a5a5848c4519","name":"Mike Bostock","login":"mbostock","bio":"Visualization toolmaker. Founder @observablehq. Creator @d3. Former @nytgraphics. Pronounced BOSS-tock.","home_url":"https://bost.ocks.org/mike/","tier":"pro","approved":true,"description":""}],"files":[{"id":"31d904f6e21d42d4963ece9c8cc4fbd75efcbdc404bf511bc79906f0a1be68b5a01e935f65123670ed04e35ca8cae3c2b943f82bf8db49c5a67c85cbb58db052","url":"https://static.observableusercontent.com/files/31d904f6e21d42d4963ece9c8cc4fbd75efcbdc404bf511bc79906f0a1be68b5a01e935f65123670ed04e35ca8cae3c2b943f82bf8db49c5a67c85cbb58db052","download_url":"https://static.observableusercontent.com/files/31d904f6e21d42d4963ece9c8cc4fbd75efcbdc404bf511bc79906f0a1be68b5a01e935f65123670ed04e35ca8cae3c2b943f82bf8db49c5a67c85cbb58db052?response-content-disposition=attachment%3Bfilename*%3DUTF-8%27%27miserables.json","name":"miserables.json","create_time":"2019-10-29T21:05:52.781Z","mime_type":"application/json","status":"public","size":18844,"content_encoding":"gzip","private_bucket_id":null}],"comments":[{"id":"937ac50580b3fe5f","content":"Hi @mbostock. If I wanted to run this function outside of the Observable environment, what implication would `invalidation` have. As I understand it, this is an object that only exists in Observable.\n\nThanks. Beautiful graph, btw.","node_id":7,"create_time":"2022-01-26T14:57:44.840Z","update_time":null,"resolved":true,"user":{"id":"c52424a79777dc03","avatar_url":"https://avatars.observableusercontent.com/avatar/6a79baea2b1a1f8cbf7fc9fd58b7a67b61b9ed43b4ecb7aa2cbd3c41df7b533d","login":"xari","name":"Harry","bio":"Educator @ [EPFL Extension School](https://www.extensionschool.ch/)","home_url":"https://www.xari.dev","tier":"public"}},{"id":"bfce8895c47e7c23","content":"invalidation is optional; if you pass a promise, the function registers \"simulation.stop\" on this promise, so it can stop doing computations when it receives that message.","node_id":7,"create_time":"2022-01-26T15:18:15.882Z","update_time":null,"resolved":true,"user":{"id":"45a379fcfcb14253","avatar_url":"https://avatars.observableusercontent.com/avatar/9cf371db2e1a2b4ed5f4a9783b6954cf7aeb2a88c56d4054c96ef360cf85ef89","login":"fil","name":"Fil","bio":"Vocateur.","home_url":"https://visionscarto.net/","tier":"pro"}},{"id":"c8b4f8c03f8da486","content":"Thanks, Fil.","node_id":7,"create_time":"2022-01-30T14:04:35.797Z","update_time":null,"resolved":true,"user":{"id":"c52424a79777dc03","avatar_url":"https://avatars.observableusercontent.com/avatar/6a79baea2b1a1f8cbf7fc9fd58b7a67b61b9ed43b4ecb7aa2cbd3c41df7b533d","login":"xari","name":"Harry","bio":"Educator @ [EPFL Extension School](https://www.extensionschool.ch/)","home_url":"https://www.xari.dev","tier":"public"}},{"id":"222d0fa1491d526c","content":"What's the point of 'replacing the input nodes and links with mutable objects for the simulation'","node_id":7,"create_time":"2022-11-29T07:38:47.350Z","update_time":null,"resolved":true,"user":{"id":"f9d57064cf004ad0","avatar_url":"https://avatars.observableusercontent.com/avatar/bb37a5423d45df1bdef69047e6b5572c6bd8c31c0112ced9f5c7f80f9aa9fd8e","login":"dianaow","name":"","bio":"Doing data viz for work and play. Particular fondness for force-directed graphs and unit visualizations.","home_url":"https://www.dianameow.com","tier":"public"}},{"id":"28d58c459ec3c8a5","content":"it would be great to give the option for node radius to insert and function to bind with data properties, as it is possibile for groups and titles","node_id":7,"create_time":"2024-03-21T08:12:21.215Z","update_time":null,"resolved":true,"user":{"id":"88179e875414a070","avatar_url":"https://avatars.observableusercontent.com/avatar/d76c5aa791b94671438fe822f26d224a383c16411fbbd792799179b3c3912918","login":"mikima","name":"Michele Mauri","bio":"","home_url":"http://densitydesign.org/","tier":"public"}},{"id":"b1699bd9ee11df37","content":"@Michele Great idea, I implemented it (you can now pass a function to nodeRadius).","node_id":7,"create_time":"2024-03-21T08:18:28.037Z","update_time":null,"resolved":true,"user":{"id":"45a379fcfcb14253","avatar_url":"https://avatars.observableusercontent.com/avatar/9cf371db2e1a2b4ed5f4a9783b6954cf7aeb2a88c56d4054c96ef360cf85ef89","login":"fil","name":"Fil","bio":"Vocateur.","home_url":"https://visionscarto.net/","tier":"pro"}},{"id":"cb94f9df61ca951b","content":"@dianaow sorry for the belated reply: we need these objects to hold the changing values of x, y, vx, vy during the simulation (as well as fx, fy if the user holds a node manually with the pointer).","node_id":7,"create_time":"2024-03-21T08:20:25.124Z","update_time":null,"resolved":true,"user":{"id":"45a379fcfcb14253","avatar_url":"https://avatars.observableusercontent.com/avatar/9cf371db2e1a2b4ed5f4a9783b6954cf7aeb2a88c56d4054c96ef360cf85ef89","login":"fil","name":"Fil","bio":"Vocateur.","home_url":"https://visionscarto.net/","tier":"pro"}},{"id":"a1a3e6ffc4cab17d","content":"@Fil wow thank you! Anther useful variable to hadle with functions would be `linkStrength`, so as `linkStrokeWidth` you can bind it to data","node_id":7,"create_time":"2024-03-21T15:41:29.945Z","update_time":"2024-03-21T15:44:47.044Z","resolved":false,"user":{"id":"88179e875414a070","avatar_url":"https://avatars.observableusercontent.com/avatar/d76c5aa791b94671438fe822f26d224a383c16411fbbd792799179b3c3912918","login":"mikima","name":"Michele Mauri","bio":"","home_url":"http://densitydesign.org/","tier":"public"}}],"commenting_lock":null,"suggestions_to":[],"suggestion_from":null,"collections":[{"id":"0a91cb79ed4a7756","type":"public","slug":"components","title":"Components","description":"Reusable data visualization components","update_time":"2023-09-27T13:12:29.794Z","pinned":false,"ordered":true,"custom_thumbnail":"6069e89e9c8e1667964b8118ad76d660def24488a082f76bbc89d08a532775e1","default_thumbnail":"e59e0961759c38381cea458fc2f328e9dab1686e04a4a240149ee2cd25558cf6","thumbnail":"6069e89e9c8e1667964b8118ad76d660def24488a082f76bbc89d08a532775e1","listing_count":13,"parent_collection_count":0,"owner":{"id":"f35c755083683fe5","avatar_url":"https://avatars.observableusercontent.com/avatar/5a51c3b908225a581d20577e488e2aba8cbc9541c52982c638638c370c3e5e8e","login":"observablehq","name":"Observable","bio":"The end-to-end solution for building and hosting better data apps, dashboards, and reports.","home_url":"https://observablehq.com","type":"team","tier":"enterprise_2024"}},{"id":"2a5376aca044b34e","type":"public","slug":"charts","title":"D3 Charts","description":"Older D3 examples, deprecated.","update_time":"2023-07-04T09:37:08.122Z","pinned":false,"ordered":true,"custom_thumbnail":null,"default_thumbnail":"2471c1145e33b39a1d5e38b7bb5460a2620aadf1c32717a270347620b1e981bd","thumbnail":"2471c1145e33b39a1d5e38b7bb5460a2620aadf1c32717a270347620b1e981bd","listing_count":11,"parent_collection_count":0,"owner":{"id":"863951e3ebe4c0ae","avatar_url":"https://avatars.observableusercontent.com/avatar/5af16e327a90b2873351dda8a596c0d2d3bf954f64523deefe80177c9764d0f7","login":"d3","name":"D3","bio":"Bring your data to life.","home_url":"https://d3js.org","type":"team","tier":"pro_2024"}},{"id":"83be77d674e06410","type":"public","slug":"d3-drag","title":"d3-drag","description":"Drag and drop SVG, HTML or Canvas using mouse or touch input.","update_time":"2019-06-30T18:01:17.368Z","pinned":false,"ordered":false,"custom_thumbnail":null,"default_thumbnail":"d1bb6653143239319cfee07a6f362c108ad0ddc9e7c064ebf185364992c2426c","thumbnail":"d1bb6653143239319cfee07a6f362c108ad0ddc9e7c064ebf185364992c2426c","listing_count":16,"parent_collection_count":1,"owner":{"id":"863951e3ebe4c0ae","avatar_url":"https://avatars.observableusercontent.com/avatar/5af16e327a90b2873351dda8a596c0d2d3bf954f64523deefe80177c9764d0f7","login":"d3","name":"D3","bio":"Bring your data to life.","home_url":"https://d3js.org","type":"team","tier":"pro_2024"}},{"id":"7c6f7ba420c1b0ee","type":"public","slug":"d3-force","title":"d3-force","description":"Force-directed graph layout using velocity Verlet integration.","update_time":"2019-03-07T07:26:05.074Z","pinned":true,"ordered":false,"custom_thumbnail":null,"default_thumbnail":"018f8185e19c3312e6f59863fbdae542db8bff9c38bfe16a2ca72ce4f19a0d1f","thumbnail":"018f8185e19c3312e6f59863fbdae542db8bff9c38bfe16a2ca72ce4f19a0d1f","listing_count":19,"parent_collection_count":1,"owner":{"id":"863951e3ebe4c0ae","avatar_url":"https://avatars.observableusercontent.com/avatar/5af16e327a90b2873351dda8a596c0d2d3bf954f64523deefe80177c9764d0f7","login":"d3","name":"D3","bio":"Bring your data to life.","home_url":"https://d3js.org","type":"team","tier":"pro_2024"}}],"version":149,"title":"Force-Directed Graph","license":"isc","copyright":"Copyright 2017–2020 Observable, Inc.","nodes":[{"id":1,"value":"md`# Force-Directed Graph\n\nThis network of character co-occurence in _Les Misérables_ is positioned by simulated forces using [d3-force](https://github.com/d3/d3-force). See also a [disconnected graph](/@d3/disjoint-force-directed-graph), and compare to [WebCoLa](/@mbostock/hello-cola).`","pinned":false,"mode":"js","data":null,"name":null},{"id":7,"value":"chart = {\n  const links = data.links.map(d => Object.create(d));\n  const nodes = data.nodes.map(d => Object.create(d));\n\n  const simulation = d3.forceSimulation(nodes)\n      .force(\"link\", d3.forceLink(links).id(d => d.id))\n      .force(\"charge\", d3.forceManyBody())\n      .force(\"center\", d3.forceCenter(width / 2, height / 2));\n\n  const svg = d3.create(\"svg\")\n      .attr(\"viewBox\", [0, 0, width, height]);\n\n  const link = svg.append(\"g\")\n      .attr(\"stroke\", \"#999\")\n      .attr(\"stroke-opacity\", 0.6)\n    .selectAll(\"line\")\n    .data(links)\n    .join(\"line\")\n      .attr(\"stroke-width\", d => Math.sqrt(d.value));\n\n  const node = svg.append(\"g\")\n      .attr(\"stroke\", \"#fff\")\n      .attr(\"stroke-width\", 1.5)\n    .selectAll(\"circle\")\n    .data(nodes)\n    .join(\"circle\")\n      .attr(\"r\", 5)\n      .attr(\"fill\", color)\n      .call(drag(simulation));\n\n  node.append(\"title\")\n      .text(d => d.id);\n\n  simulation.on(\"tick\", () => {\n    link\n        .attr(\"x1\", d => d.source.x)\n        .attr(\"y1\", d => d.source.y)\n        .attr(\"x2\", d => d.target.x)\n        .attr(\"y2\", d => d.target.y);\n\n    node\n        .attr(\"cx\", d => d.x)\n        .attr(\"cy\", d => d.y);\n  });\n\n  invalidation.then(() => simulation.stop());\n\n  return svg.node();\n}","pinned":true,"mode":"js","data":null,"name":null},{"id":3,"value":"data = FileAttachment(\"miserables.json\").json()","pinned":true,"mode":"js","data":null,"name":null},{"id":32,"value":"height = 600","pinned":true,"mode":"js","data":null,"name":null},{"id":26,"value":"color = {\n  const scale = d3.scaleOrdinal(d3.schemeCategory10);\n  return d => scale(d.group);\n}","pinned":true,"mode":"js","data":null,"name":null},{"id":76,"value":"drag = simulation => {\n  \n  function dragstarted(event) {\n    if (!event.active) simulation.alphaTarget(0.3).restart();\n    event.subject.fx = event.subject.x;\n    event.subject.fy = event.subject.y;\n  }\n  \n  function dragged(event) {\n    event.subject.fx = event.x;\n    event.subject.fy = event.y;\n  }\n  \n  function dragended(event) {\n    if (!event.active) simulation.alphaTarget(0);\n    event.subject.fx = null;\n    event.subject.fy = null;\n  }\n  \n  return d3.drag()\n      .on(\"start\", dragstarted)\n      .on(\"drag\", dragged)\n      .on(\"end\", dragended);\n}","pinned":true,"mode":"js","data":null,"name":null},{"id":11,"value":"d3 = require(\"d3@6\")","pinned":true,"mode":"js","data":null,"name":null}],"resolutions":[],"schedule":null,"last_view_time":null}