SolidJS-forceDirectedGraph(1)

使用solidJS实现力导向图

力导向图参考:https://observablehq.com/@d3/force-directed-graph/2

原始代码与分析

chart = {
  // Specify the dimensions of the chart.
  const width = 928;
  const height = 600;

  // Specify the color scale.
  const color = d3.scaleOrdinal(d3.schemeCategory10);

  // The force simulation mutates links and nodes, so create a copy
  // so that re-evaluating this cell produces the same result.
  const links = data.links.map(d => ({...d}));
  const nodes = data.nodes.map(d => ({...d}));

  // Create a simulation with several forces.
  const simulation = d3.forceSimulation(nodes)
      .force("link", d3.forceLink(links).id(d => d.id))
      .force("charge", d3.forceManyBody())
      .force("center", d3.forceCenter(width / 2, height / 2))
      .on("tick", ticked);

  // Create the SVG container.
  const svg = d3.create("svg")
      .attr("width", width)
      .attr("height", height)
      .attr("viewBox", [0, 0, width, height])
      .attr("style", "max-width: 100%; height: auto;");

  // Add a line for each link, and a circle for each node.
  const link = svg.append("g")
      .attr("stroke", "#999")
      .attr("stroke-opacity", 0.6)
    .selectAll()
    .data(links)
    .join("line")
      .attr("stroke-width", d => Math.sqrt(d.value));

  const node = svg.append("g")
      .attr("stroke", "#fff")
      .attr("stroke-width", 1.5)
    .selectAll()
    .data(nodes)
    .join("circle")
      .attr("r", 5)
      .attr("fill", d => color(d.group));

  node.append("title")
      .text(d => d.id);

  // Add a drag behavior.
  node.call(d3.drag()
        .on("start", dragstarted)
        .on("drag", dragged)
        .on("end", dragended));

  // Set the position attributes of links and nodes each time the simulation ticks.
  function ticked() {
    link
        .attr("x1", d => d.source.x)
        .attr("y1", d => d.source.y)
        .attr("x2", d => d.target.x)
        .attr("y2", d => d.target.y);

    node
        .attr("cx", d => d.x)
        .attr("cy", d => d.y);
  }

  // Reheat the simulation when drag starts, and fix the subject position.
  function dragstarted(event) {
    if (!event.active) simulation.alphaTarget(0.3).restart();
    event.subject.fx = event.subject.x;
    event.subject.fy = event.subject.y;
  }

  // Update the subject (dragged node) position during drag.
  function dragged(event) {
    event.subject.fx = event.x;
    event.subject.fy = event.y;
  }

  // Restore the target alpha so the simulation cools after dragging ends.
  // Unfix the subject position now that it’s no longer being dragged.
  function dragended(event) {
    if (!event.active) simulation.alphaTarget(0);
    event.subject.fx = null;
    event.subject.fy = null;
  }

  // When this cell is re-run, stop the previous simulation. (This doesn’t
  // really matter since the target alpha is zero and the simulation will
  // stop naturally, but it’s a good practice.)
  invalidation.then(() => simulation.stop());

  return svg.node();
}

GPT分析

这段代码使用D3.js库创建了一个力导向图(force-directed graph)。力导向图是一种通过模拟物理力的方式来自动布局图形节点的图表。下面是对这段代码的详细分析:

  1. 设置图表的尺寸:

    const width = 928;
    const height = 600;
    

    图表的宽度被设置为928像素,高度设置为600像素。

  2. 设置颜色比例尺:

    const color = d3.scaleOrdinal(d3.schemeCategory10);
    

    使用D3内置的颜色比例尺d3.schemeCategory10来为不同的节点分组设置颜色。

  3. 复制链接和节点的数据:

    const links = data.links.map(d => ({...d}));
    const nodes = data.nodes.map(d => ({...d}));
    

    为了防止原始数据被d3.forceSimulation改变,创建链接和节点数据的副本。

  4. 创建力导向仿真:

    const simulation = d3.forceSimulation(nodes)
        .force("link", d3.forceLink(links).id(d => d.id))
        .force("charge", d3.forceManyBody())
        .force("center", d3.forceCenter(width / 2, height / 2))
        .on("tick", ticked);
    

    创建一个力导向仿真,包括以下几个力:

    • forceLink:链接力,用于连接节点。
    • forceManyBody:多体力,用于节点之间的相互作用。
    • forceCenter:中心力,用于将图形居中。
      每次仿真“tick”时调用ticked函数。
  5. 创建SVG容器:

    const svg = d3.create("svg")
        .attr("width", width)
        .attr("height", height)
        .attr("viewBox", [0, 0, width, height])
        .attr("style", "max-width: 100%; height: auto;");
    

    创建一个SVG元素并设置其属性。

  6. 添加链接线和节点圆圈:

    const link = svg.append("g")
        .attr("stroke", "#999")
        .attr("stroke-opacity", 0.6)
      .selectAll()
      .data(links)
      .join("line")
        .attr("stroke-width", d => Math.sqrt(d.value));
    
    const node = svg.append("g")
        .attr("stroke", "#fff")
        .attr("stroke-width", 1.5)
      .selectAll()
      .data(nodes)
      .join("circle")
        .attr("r", 5)
        .attr("fill", d => color(d.group));
    

    添加链接线,设置其颜色、透明度和线宽。然后添加节点圆圈,设置其半径、填充颜色和边框。

  7. 添加节点拖拽行为:

    node.call(d3.drag()
          .on("start", dragstarted)
          .on("drag", dragged)
          .on("end", dragended));
    

    为节点添加拖拽行为,定义拖拽开始、进行中和结束时的回调函数。

  8. 每次仿真“tick”时更新链接和节点的位置:

    function ticked() {
      link
          .attr("x1", d => d.source.x)
          .attr("y1", d => d.source.y)
          .attr("x2", d => d.target.x)
          .attr("y2", d => d.target.y);
    
      node
          .attr("cx", d => d.x)
          .attr("cy", d => d.y);
    }
    

    每次仿真“tick”时,更新链接线和节点圆圈的位置。

  9. 拖拽行为的回调函数:

    function dragstarted(event) {
      if (!event.active) simulation.alphaTarget(0.3).restart();
      event.subject.fx = event.subject.x;
      event.subject.fy = event.subject.y;
    }
    
    function dragged(event) {
      event.subject.fx = event.x;
      event.subject.fy = event.y;
    }
    
    function dragended(event) {
      if (!event.active) simulation.alphaTarget(0);
      event.subject.fx = null;
      event.subject.fy = null;
    }
    

    dragstarted函数在拖拽开始时重新加热仿真并固定节点位置,dragged函数在拖拽过程中更新节点位置,dragended函数在拖拽结束时恢复仿真目标alpha并解除节点位置的固定。

  10. 停止之前的仿真:

    invalidation.then(() => simulation.stop());
    

    当该单元格重新运行时,停止之前的仿真。

  11. 返回SVG节点:

    return svg.node();
    

总的来说,这段代码定义了一个D3.js力导向图,其中包括节点和链接的绘制、力仿真的设置以及拖拽行为的实现。

使用SolidJS和TS实现以上功能(GPT)

要使用SolidJS和TypeScript实现力导向图,我们需要以下步骤:

  1. 设置项目:初始化SolidJS项目并配置TypeScript支持。
  2. 定义力导向图的算法:由于不使用额外的库,我们需要手动实现力导向仿真。
  3. 绘制SVG:使用SolidJS的模板语法创建SVG元素并渲染节点和连线。
  4. 实现节点拖拽功能:在SVG中实现节点的拖拽行为。

以下是一个完整的示例,展示了如何在SolidJS和TypeScript中实现上述功能:

1. 设置项目

确保你已经安装了Node.js和npm。然后,创建一个新的SolidJS项目并安装必要的依赖项。

npm init solid my-solid-project
cd my-solid-project
npm install
npm install typescript @types/node --save-dev

配置TypeScript支持:

tsconfig.json文件中添加以下内容:

{
  "compilerOptions": {
    "target": "esnext",
    "module": "esnext",
    "jsx": "preserve",
    "jsxImportSource": "solid-js",
    "strict": true,
    "moduleResolution": "node",
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true
  },
  "include": ["src"]
}

2. 定义力导向图的算法

首先,我们需要定义一些接口来描述图的数据结构:

interface Node {
  id: string;
  group: number;
  x?: number;
  y?: number;
  vx?: number;
  vy?: number;
  fx?: number | null;
  fy?: number | null;
}

interface Link {
  source: string;
  target: string;
  value: number;
}

interface GraphData {
  nodes: Node[];
  links: Link[];
}

3. 实现力导向仿真

实现简单的力导向仿真算法:

const width = 928;
const height = 600;

function forceSimulation(nodes: Node[], links: Link[]) {
  // Initialize positions if they are not already set
  nodes.forEach((node) => {
    node.x = node.x ?? Math.random() * width;
    node.y = node.y ?? Math.random() * height;
    node.vx = 0;
    node.vy = 0;
  });

  // Simplified force-directed algorithm
  const alpha = 0.1;
  const alphaDecay = 0.99;
  const repulsionStrength = -30;
  const attractionStrength = 0.1;

  function tick() {
    // Apply repulsion between nodes
    for (let i = 0; i < nodes.length; i++) {
      for (let j = i + 1; j < nodes.length; j++) {
        const dx = nodes[j].x! - nodes[i].x!;
        const dy = nodes[j].y! - nodes[i].y!;
        const distance = Math.sqrt(dx * dx + dy * dy) || 1;
        const force = repulsionStrength / (distance * distance);
        nodes[i].vx! -= force * dx;
        nodes[i].vy! -= force * dy;
        nodes[j].vx! += force * dx;
        nodes[j].vy! += force * dy;
      }
    }

    // Apply attraction along links
    links.forEach((link) => {
      const source = nodes.find((node) => node.id === link.source)!;
      const target = nodes.find((node) => node.id === link.target)!;
      const dx = target.x! - source.x!;
      const dy = target.y! - source.y!;
      const distance = Math.sqrt(dx * dx + dy * dy) || 1;
      const force = (distance - 30) * attractionStrength;
      const fx = (dx / distance) * force;
      const fy = (dy / distance) * force;
      source.vx! += fx;
      source.vy! += fy;
      target.vx! -= fx;
      target.vy! -= fy;
    });

    // Update positions
    nodes.forEach((node) => {
      if (!node.fx) node.x! += node.vx! * alpha;
      if (!node.fy) node.y! += node.vy! * alpha;
      node.vx! *= alphaDecay;
      node.vy! *= alphaDecay;
    });
  }

  return { tick };
}

4. 绘制SVG并实现拖拽

在SolidJS组件中实现SVG绘制和拖拽行为:

import { createSignal, onCleanup, onMount } from "solid-js";
import { render } from "solid-js/web";

const Graph = (props: { data: GraphData }) => {
  const [nodes, setNodes] = createSignal<Node[]>(props.data.nodes);
  const [links] = createSignal<Link[]>(props.data.links);

  let svgRef: SVGSVGElement | undefined;

  onMount(() => {
    const simulation = forceSimulation(nodes(), links());
    const interval = setInterval(() => {
      simulation.tick();
      setNodes([...nodes()]);
    }, 100);

    onCleanup(() => clearInterval(interval));
  });

  const drag = (event: MouseEvent, node: Node) => {
    node.fx = event.clientX - (svgRef?.getBoundingClientRect().left || 0);
    node.fy = event.clientY - (svgRef?.getBoundingClientRect().top || 0);
    setNodes([...nodes()]);
  };

  const endDrag = (node: Node) => {
    node.fx = null;
    node.fy = null;
    setNodes([...nodes()]);
  };

  return (
    <svg
      ref={svgRef}
      width={width}
      height={height}
      style={{ border: "1px solid black", "max-width": "100%", height: "auto" }}
    >
      {links().map((link) => {
        const source = nodes().find((node) => node.id === link.source)!;
        const target = nodes().find((node) => node.id === link.target)!;
        return (
          <line
            x1={source.x}
            y1={source.y}
            x2={target.x}
            y2={target.y}
            stroke="#999"
            stroke-opacity="0.6"
            stroke-width={Math.sqrt(link.value)}
          />
        );
      })}
      {nodes().map((node) => (
        <circle
          cx={node.x}
          cy={node.y}
          r="5"
          fill="#69b3a2"
          stroke="#fff"
          stroke-width="1.5"
          onmousedown={(e) => drag(e, node)}
          onmouseup={() => endDrag(node)}
        >
          <title>{node.id}</title>
        </circle>
      ))}
    </svg>
  );
};

const data: GraphData = {
  nodes: [
    { id: "1", group: 1 },
    { id: "2", group: 2 },
    { id: "3", group: 2 },
    { id: "4", group: 1 },
  ],
  links: [
    { source: "1", target: "2", value: 1 },
    { source: "2", target: "3", value: 1 },
    { source: "3", target: "4", value: 1 },
  ],
};

render(() => <Graph data={data} />, document.getElementById("root"));

这个示例展示了如何在SolidJS中实现一个简单的力导向图。我们手动实现了力导向仿真算法,并使用SVG绘制图形,同时实现了节点的拖拽功能。通过这种方式,可以更好地理解力导向图的原理和实现细节。


将代码分为多个文件有助于代码的可维护性和模块化。可以按照功能划分不同的文件,如下所示:

  1. types.ts:定义图的数据结构。
  2. forceSimulation.ts:实现力导向仿真算法。
  3. Graph.tsx:使用SolidJS创建图表组件。

下面是如何组织这些文件:

1. types.ts

export interface Node {
  id: string;
  group: number;
  x?: number;
  y?: number;
  vx?: number;
  vy?: number;
  fx?: number | null;
  fy?: number | null;
}

export interface Link {
  source: string;
  target: string;
  value: number;
}

export interface GraphData {
  nodes: Node[];
  links: Link[];
}

2. forceSimulation.ts

import { Node, Link } from "./types";

const width = 928;
const height = 600;

export function forceSimulation(nodes: Node[], links: Link[]) {
  nodes.forEach((node) => {
    node.x = node.x ?? Math.random() * width;
    node.y = node.y ?? Math.random() * height;
    node.vx = 0;
    node.vy = 0;
  });

  const alpha = 0.1;
  const alphaDecay = 0.99;
  const repulsionStrength = -30;
  const attractionStrength = 0.1;

  function tick() {
    for (let i = 0; i < nodes.length; i++) {
      for (let j = i + 1; j < nodes.length; j++) {
        const dx = nodes[j].x! - nodes[i].x!;
        const dy = nodes[j].y! - nodes[i].y!;
        const distance = Math.sqrt(dx * dx + dy * dy) || 1;
        const force = repulsionStrength / (distance * distance);
        nodes[i].vx! -= force * dx;
        nodes[i].vy! -= force * dy;
        nodes[j].vx! += force * dx;
        nodes[j].vy! += force * dy;
      }
    }

    links.forEach((link) => {
      const source = nodes.find((node) => node.id === link.source)!;
      const target = nodes.find((node) => node.id === link.target)!;
      const dx = target.x! - source.x!;
      const dy = target.y! - source.y!;
      const distance = Math.sqrt(dx * dx + dy * dy) || 1;
      const force = (distance - 30) * attractionStrength;
      const fx = (dx / distance) * force;
      const fy = (dy / distance) * force;
      source.vx! += fx;
      source.vy! += fy;
      target.vx! -= fx;
      target.vy! -= fy;
    });

    nodes.forEach((node) => {
      if (!node.fx) node.x! += node.vx! * alpha;
      if (!node.fy) node.y! += node.vy! * alpha;
      node.vx! *= alphaDecay;
      node.vy! *= alphaDecay;
    });
  }

  return { tick };
}

3. Graph.tsx

import { createSignal, onCleanup, onMount } from "solid-js";
import { Node, Link, GraphData } from "./types";
import { forceSimulation } from "./forceSimulation";

const width = 928;
const height = 600;

const Graph = (props: { data: GraphData }) => {
  const [nodes, setNodes] = createSignal<Node[]>(props.data.nodes);
  const [links] = createSignal<Link[]>(props.data.links);

  let svgRef: SVGSVGElement | undefined;

  onMount(() => {
    const simulation = forceSimulation(nodes(), links());
    const interval = setInterval(() => {
      simulation.tick();
      setNodes([...nodes()]);
    }, 100);

    onCleanup(() => clearInterval(interval));
  });

  const drag = (event: MouseEvent, node: Node) => {
    node.fx = event.clientX - (svgRef?.getBoundingClientRect().left || 0);
    node.fy = event.clientY - (svgRef?.getBoundingClientRect().top || 0);
    setNodes([...nodes()]);
  };

  const endDrag = (node: Node) => {
    node.fx = null;
    node.fy = null;
    setNodes([...nodes()]);
  };

  return (
    <svg
      ref={svgRef}
      width={width}
      height={height}
      style={{ border: "1px solid black", "max-width": "100%", height: "auto" }}
    >
      {links().map((link) => {
        const source = nodes().find((node) => node.id === link.source)!;
        const target = nodes().find((node) => node.id === link.target)!;
        return (
          <line
            x1={source.x}
            y1={source.y}
            x2={target.x}
            y2={target.y}
            stroke="#999"
            stroke-opacity="0.6"
            stroke-width={Math.sqrt(link.value)}
          />
        );
      })}
      {nodes().map((node) => (
        <circle
          cx={node.x}
          cy={node.y}
          r="5"
          fill="#69b3a2"
          stroke="#fff"
          stroke-width="1.5"
          onmousedown={(e) => drag(e, node)}
          onmouseup={() => endDrag(node)}
        >
          <title>{node.id}</title>
        </circle>
      ))}
    </svg>
  );
};

export default Graph;

4. index.tsx

最后,在入口文件中渲染Graph组件:

import { render } from "solid-js/web";
import Graph from "./Graph";
import { GraphData } from "./types";

const data: GraphData = {
  nodes: [
    { id: "1", group: 1 },
    { id: "2", group: 2 },
    { id: "3", group: 2 },
    { id: "4", group: 1 },
  ],
  links: [
    { source: "1", target: "2", value: 1 },
    { source: "2", target: "3", value: 1 },
    { source: "3", target: "4", value: 1 },
  ],
};

render(() => <Graph data={data} />, document.getElementById("root"));

总结

通过这种方式,我们将代码分为多个文件,使得每个文件专注于一个功能。这样做不仅提高了代码的可读性和可维护性,还便于重用和测试。

停止仿真(待解决)

在力导向图中,节点持续运动是由于力仿真算法在不断地调整节点和连线的位置,以达到一个相对稳定的状态。这种运动是由仿真中的力(如引力、排斥力和连线力)驱动的。仿真会在每个时间步(tick)调整节点的位置,直到系统达到平衡。

然而,仿真不应该无限期地运行。通常,当仿真达到平衡或经过一定的时间后,会停止仿真,使节点保持静止状态。

在你的实现中,我们可以添加一个停止仿真的机制,当仿真达到一定的稳定性时停止。

修改 forceSimulation 以支持停止仿真

我们可以添加一个alpha阈值,当alpha值低于这个阈值时,停止仿真。这样,当系统达到平衡状态时,仿真就会停止。

forceSimulation.ts 中,修改 forceSimulation 函数:

import { Node, Link } from "./types";

const width = 928;
const height = 600;

export function forceSimulation(nodes: Node[], links: Link[]) {
  nodes.forEach((node) => {
    node.x = node.x ?? Math.random() * width;
    node.y = node.y ?? Math.random() * height;
    node.vx = 0;
    node.vy = 0;
  });

  const alphaDecay = 0.99;
  const repulsionStrength = -30;
  const attractionStrength = 0.1;
  let alpha = 0.1;
  const alphaMin = 0.001; // Minimum alpha value to stop the simulation

  function tick() {
    if (alpha < alphaMin) return false; // Stop simulation when alpha is low

    for (let i = 0; i < nodes.length; i++) {
      for (let j = i + 1; j < nodes.length; j++) {
        const dx = nodes[j].x! - nodes[i].x!;
        const dy = nodes[j].y! - nodes[i].y!;
        const distance = Math.sqrt(dx * dx + dy * dy) || 1;
        const force = repulsionStrength / (distance * distance);
        nodes[i].vx! -= force * dx;
        nodes[i].vy! -= force * dy;
        nodes[j].vx! += force * dx;
        nodes[j].vy! += force * dy;
      }
    }

    links.forEach((link) => {
      const source = nodes.find((node) => node.id === link.source)!;
      const target = nodes.find((node) => node.id === link.target)!;
      const dx = target.x! - source.x!;
      const dy = target.y! - source.y!;
      const distance = Math.sqrt(dx * dx + dy * dy) || 1;
      const force = (distance - 30) * attractionStrength;
      const fx = (dx / distance) * force;
      const fy = (dy / distance) * force;
      source.vx! += fx;
      source.vy! += fy;
      target.vx! -= fx;
      target.vy! -= fy;
    });

    nodes.forEach((node) => {
      if (!node.fx) node.x! += node.vx! * alpha;
      if (!node.fy) node.y! += node.vy! * alpha;
      node.vx! *= alphaDecay;
      node.vy! *= alphaDecay;
    });

    alpha *= alphaDecay; // Decrease alpha

    return true; // Continue simulation
  }

  return { tick };
}

修改 Graph.tsx 以停止仿真

Graph.tsx 中,修改 onMount 函数,以停止仿真:

import { createSignal, onCleanup, onMount } from "solid-js";
import { Node, Link, GraphData } from "./types";
import { forceSimulation } from "./forceSimulation";

const width = 928;
const height = 600;

const Graph = (props: { data: GraphData }) => {
  const [nodes, setNodes] = createSignal<Node[]>(props.data.nodes);
  const [links] = createSignal<Link[]>(props.data.links);

  let svgRef: SVGSVGElement | undefined;

  onMount(() => {
    const simulation = forceSimulation(nodes(), links());
    const interval = setInterval(() => {
      const running = simulation.tick();
      setNodes([...nodes()]);
      if (!running) {
        clearInterval(interval); // Stop the interval if simulation is not running
      }
    }, 100);

    onCleanup(() => clearInterval(interval));
  });

  const drag = (event: MouseEvent, node: Node) => {
    node.fx = event.clientX - (svgRef?.getBoundingClientRect().left || 0);
    node.fy = event.clientY - (svgRef?.getBoundingClientRect().top || 0);
    setNodes([...nodes()]);
  };

  const endDrag = (node: Node) => {
    node.fx = null;
    node.fy = null;
    setNodes([...nodes()]);
  };

  return (
    <svg
      ref={svgRef}
      width={width}
      height={height}
      style={{ border: "1px solid black", "max-width": "100%", height: "auto" }}
    >
      {links().map((link) => {
        const source = nodes().find((node) => node.id === link.source)!;
        const target = nodes().find((node) => node.id === link.target)!;
        return (
          <line
            x1={source.x}
            y1={source.y}
            x2={target.x}
            y2={target.y}
            stroke="#999"
            stroke-opacity="0.6"
            stroke-width={Math.sqrt(link.value)}
          />
        );
      })}
      {nodes().map((node) => (
        <circle
          cx={node.x}
          cy={node.y}
          r="5"
          fill="#69b3a2"
          stroke="#fff"
          stroke-width="1.5"
          onmousedown={(e) => drag(e, node)}
          onmouseup={() => endDrag(node)}
        >
          <title>{node.id}</title>
        </circle>
      ))}
    </svg>
  );
};

export default Graph;

通过这些修改,当仿真达到稳定状态(即alpha值低于某个阈值)时,仿真将自动停止,从而使节点不再继续运动。

现存问题

  1. 点的初始状态不是静止的。加入仿真停止阈值后,点由运动状态逐渐转为静止
  2. 没有拖拽功能
  3. 初始点和link数量太少(有无数据集?)
posted @ 2024-07-23 11:15  梧桐灯下江楚滢  阅读(2)  评论(0编辑  收藏  举报