{"id":"a6c00de7c09bdfa1","slug":"force-directed-graph-component","trashed":false,"description":"","likes":417,"publish_level":"live","forks":922,"fork_of":null,"has_importers":true,"update_time":"2024-09-23T09:37:35.466Z","first_public_version":327,"paused_version":null,"publish_time":"2023-09-27T12:51:27.464Z","publish_version":340,"latest_version":340,"thumbnail":"dfe10335d495307d951f90ca2a21f6539a498d22a51f54d0c9f366af47dd996d","default_thumbnail":"dfe10335d495307d951f90ca2a21f6539a498d22a51f54d0c9f366af47dd996d","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":""}],"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"}}],"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"}},{"id":"cd70a7d2affa4adb","content":"Thanks for providing the notebook! \nI'm trying to use your code outside of observablehq in a \"traditional html-javascript constellation\". I'm importing D3 via a CDN. As promised, your function ForceGraph() returns a <svg> element. However, I'm failing to append it to the DOM. By listening to the event 'DOMContentLoaded' I make sure, that D3 can access my target element. Can you please give me hint, how to append the <svg> to the DOM?\n\nThanks in advance\nAlexander","node_id":158,"create_time":"2022-10-14T19:14:43.386Z","update_time":null,"resolved":true,"user":{"id":"511f949aef207ac9","avatar_url":"https://avatars.observableusercontent.com/avatar/cbd4d07b6a2c204f7ee9ec682fb8f8737a556ce571f7a75b81f681f1ebdf00d1","login":"alexandergantikow","name":"Alexander","bio":"","home_url":"","tier":"public"}},{"id":"0cefd9e58401f673","content":"Same issue on my side. I tried to append the created svg into my html by using: \n\nd3.select('body').append('chart');\n\nBut it looks it's still the wrong way to go. No graph appear in the web page but at least no error either.\n\nAny hint how to do it?","node_id":158,"create_time":"2023-02-23T15:57:42.670Z","update_time":null,"resolved":true,"user":{"id":"16b6111fd6d0b43a","avatar_url":"https://avatars.observableusercontent.com/avatar/660908d811313db1a1fcbcf296bc290859bb843e73cccb83cb3595c6b83612e7","login":"sheaven","name":"Hervé","bio":"","home_url":"","tier":"public"}},{"id":"a432b707dfdc75ff","content":"If _chart_ is the result of the chart cell above, you could try to use:\n   d3.select('body').append(() => chart);\n\nSee https://github.com/d3/d3-selection#selection_append","node_id":158,"create_time":"2023-02-23T16:06:26.208Z","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":"9bf259dd8d0cec0a","content":"Great! It did it, now the graph pops up in the web page.\nThx a lot for your help.","node_id":158,"create_time":"2023-02-23T16:11:06.366Z","update_time":null,"resolved":true,"user":{"id":"16b6111fd6d0b43a","avatar_url":"https://avatars.observableusercontent.com/avatar/660908d811313db1a1fcbcf296bc290859bb843e73cccb83cb3595c6b83612e7","login":"sheaven","name":"Hervé","bio":"","home_url":"","tier":"public"}}],"commenting_lock":null,"suggestion_from":null,"suggestions_to":[],"version":340,"title":"Force-directed graph component","license":"isc","copyright":"Copyright 2017–2023 Observable, Inc.","nodes":[{"id":1,"value":"# Force-directed graph component\n\nThis network of character co-occurence in _Les Misérables_ is positioned using [D3’s force layout](https://github.com/d3/d3-force). Color represents arbitrary clusters in the data. Drag nodes below to better understand connections. See also a [disconnected graph](/@d3/disjoint-force-directed-graph/2), a [canvas version](/@d3/force-directed-graph-canvas/2), and compare to [WebCoLa](/@mbostock/hello-cola). Data: [Stanford Graph Base](https://www-cs-faculty.stanford.edu/~knuth/sgb.html)","pinned":false,"mode":"md","data":null,"name":""},{"id":159,"value":"chart = ForceGraph(miserables, {\n  nodeId: d => d.id,\n  nodeGroup: d => d.group,\n  nodeTitle: d => `${d.id}\\n${d.group}`,\n  linkStrokeWidth: l => Math.sqrt(l.value),\n  width,\n  height: 600,\n  invalidation // a promise to stop the simulation when the cell is re-run\n})","pinned":true,"mode":"js","data":null,"name":null},{"id":3,"value":"miserables = FileAttachment(\"miserables.json\").json()","pinned":true,"mode":"js","data":null,"name":null},{"id":158,"value":"howto(\"ForceGraph\", {alternatives: `[Force-directed graph example](/@d3/force-directed-graph/2)`})","pinned":false,"mode":"js","data":null,"name":null},{"id":7,"value":"// Copyright 2021-2024 Observable, Inc.\n// Released under the ISC license.\n// https://observablehq.com/@d3/force-directed-graph\nfunction ForceGraph({\n  nodes, // an iterable of node objects (typically [{id}, …])\n  links // an iterable of link objects (typically [{source, target}, …])\n}, {\n  nodeId = d => d.id, // given d in nodes, returns a unique identifier (string)\n  nodeGroup, // given d in nodes, returns an (ordinal) value for color\n  nodeGroups, // an array of ordinal values representing the node groups\n  nodeTitle, // given d in nodes, a title string\n  nodeFill = \"currentColor\", // node stroke fill (if not using a group color encoding)\n  nodeStroke = \"#fff\", // node stroke color\n  nodeStrokeWidth = 1.5, // node stroke width, in pixels\n  nodeStrokeOpacity = 1, // node stroke opacity\n  nodeRadius = 5, // node radius, in pixels\n  nodeStrength,\n  linkSource = ({source}) => source, // given d in links, returns a node identifier string\n  linkTarget = ({target}) => target, // given d in links, returns a node identifier string\n  linkStroke = \"#999\", // link stroke color\n  linkStrokeOpacity = 0.6, // link stroke opacity\n  linkStrokeWidth = 1.5, // given d in links, returns a stroke width in pixels\n  linkStrokeLinecap = \"round\", // link stroke linecap\n  linkStrength,\n  colors = d3.schemeTableau10, // an array of color strings, for the node groups\n  width = 640, // outer width, in pixels\n  height = 400, // outer height, in pixels\n  invalidation // when this promise resolves, stop the simulation\n} = {}) {\n  // Compute values.\n  const N = d3.map(nodes, nodeId).map(intern);\n  const R = typeof nodeRadius !== \"function\" ? null : d3.map(nodes, nodeRadius);\n  const LS = d3.map(links, linkSource).map(intern);\n  const LT = d3.map(links, linkTarget).map(intern);\n  if (nodeTitle === undefined) nodeTitle = (_, i) => N[i];\n  const T = nodeTitle == null ? null : d3.map(nodes, nodeTitle);\n  const G = nodeGroup == null ? null : d3.map(nodes, nodeGroup).map(intern);\n  const W = typeof linkStrokeWidth !== \"function\" ? null : d3.map(links, linkStrokeWidth);\n  const L = typeof linkStroke !== \"function\" ? null : d3.map(links, linkStroke);\n  \n\n  // Replace the input nodes and links with mutable objects for the simulation.\n  nodes = d3.map(nodes, (_, i) => ({id: N[i]}));\n  links = d3.map(links, (_, i) => ({source: LS[i], target: LT[i]}));\n\n  // Compute default domains.\n  if (G && nodeGroups === undefined) nodeGroups = d3.sort(G);\n\n  // Construct the scales.\n  const color = nodeGroup == null ? null : d3.scaleOrdinal(nodeGroups, colors);\n\n  // Construct the forces.\n  const forceNode = d3.forceManyBody();\n  const forceLink = d3.forceLink(links).id(({index: i}) => N[i]);\n  if (nodeStrength !== undefined) forceNode.strength(nodeStrength);\n  if (linkStrength !== undefined) forceLink.strength(linkStrength);\n\n  const simulation = d3.forceSimulation(nodes)\n      .force(\"link\", forceLink)\n      .force(\"charge\", forceNode)\n      .force(\"center\",  d3.forceCenter())\n      .on(\"tick\", ticked);\n\n  const svg = d3.create(\"svg\")\n      .attr(\"width\", width)\n      .attr(\"height\", height)\n      .attr(\"viewBox\", [-width / 2, -height / 2, width, height])\n      .attr(\"style\", \"max-width: 100%; height: auto; height: intrinsic;\");\n\n  const link = svg.append(\"g\")\n      .attr(\"stroke\", typeof linkStroke !== \"function\" ? linkStroke : null)\n      .attr(\"stroke-opacity\", linkStrokeOpacity)\n      .attr(\"stroke-width\", typeof linkStrokeWidth !== \"function\" ? linkStrokeWidth : null)\n      .attr(\"stroke-linecap\", linkStrokeLinecap)\n    .selectAll(\"line\")\n    .data(links)\n    .join(\"line\");\n\n  const node = svg.append(\"g\")\n      .attr(\"fill\", nodeFill)\n      .attr(\"stroke\", nodeStroke)\n      .attr(\"stroke-opacity\", nodeStrokeOpacity)\n      .attr(\"stroke-width\", nodeStrokeWidth)\n    .selectAll(\"circle\")\n    .data(nodes)\n    .join(\"circle\")\n      .attr(\"r\", nodeRadius)\n      .call(drag(simulation));\n\n  if (W) link.attr(\"stroke-width\", ({index: i}) => W[i]);\n  if (L) link.attr(\"stroke\", ({index: i}) => L[i]);\n  if (G) node.attr(\"fill\", ({index: i}) => color(G[i]));\n  if (R) node.attr(\"r\", ({index: i}) => R[i]);\n  if (T) node.append(\"title\").text(({index: i}) => T[i]);\n  if (invalidation != null) invalidation.then(() => simulation.stop());\n\n  function intern(value) {\n    return value !== null && typeof value === \"object\" ? value.valueOf() : value;\n  }\n\n  function ticked() {\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  function drag(simulation) {    \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  }\n\n  return Object.assign(svg.node(), {scales: {color}});\n}","pinned":true,"mode":"js","data":null,"name":null},{"id":154,"value":"import {howto} from \"@d3/example-components\"","pinned":true,"mode":"js","data":null,"name":null},{"id":290,"value":"import {Swatches} from \"@d3/color-legend\"","pinned":true,"mode":"js","data":null,"name":null}],"resolutions":[],"schedule":null,"last_view_time":null}