ロゴ メインコンテンツへ
RSSフィード
「Web 開発」に関連する記事一覧

d3.js 円型ノードの中心同士を繋ぐノードグラフサンプル

2023/10/23
(この記事の文字数: 86)

d3.js で円型ノードの中心同士を繋ぐノードグラフのサンプルです。ノードはドラッグ移動できます。

ソースコード。無駄にclass化されてますが、その辺は目をつぶってください。

<svg width="400" height="400">
<defs>
  <filter id="solid">
    <feFlood flood-color="#eeeeee" result="bg" flood-opacity="0.8" />
    <feMerge>
      <feMergeNode in="bg" />
      <feMergeNode in="SourceGraphic" />
    </feMerge>
  </filter>
</defs>
</svg>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/7.8.5/d3.min.js"
integrity="sha512-M7nHCiNUOwFt6Us3r8alutZLm9qMt4s9951uo8jqO4UwJ1hziseL6O3ndFyigx6+LREfZqnhHxYjKRJ8ZQ69DQ=="
crossorigin="anonymous" referrerpolicy="no-referrer"></script>

<script>
class Node {
  constructor(
    index,
    label,
    x, y, radius, width, height, href,
    imageOffsetX, imageOffsetY,
    forceRateY = 1.0
  ) {
    this.index = index;
    this.label = label;
    this.x = x;
    this.y = y;
    this.radius = radius;
    this.width = width;
    this.height = height;
    this.href = href;
    this.imageOffsetX = imageOffsetX;
    this.imageOffsetY = imageOffsetY;
    this.forceRateY = forceRateY;
  }
}

class NodeEdge {
  constructor(label, source, target, distance) {
    this.label = label;
    this.labelOffset = 6;
    this.source = source;
    this.target = target;
    this.distance = distance;
  }
}

class RelationshipChart {
  constructor() {
    this.simulation = null;
    this.nodesData = [];
    this.linksData = [];
    this.link = null;
    this.edgeLabels = null;
    this.node = null;
    this.imageFrame = null;
    this.nodeLabels = null;
    this.clipPaths = null;
  }

  initMain() {
    const imageWidth = 100;
    const imageHeight = 100;
    const imageFrameInnerWidth = 3;
    const imageFrameOuterWidth = 2;
    const imageFrameColor = "#ffdd99";
    const imageFrameOuterColor = "#ffaa00";
    const fontSize = 10;

    const width = document.querySelector("svg").clientWidth;
    const height = document.querySelector("svg").clientHeight;

    this.nodesData.push(new Node(0,
      "アルフレッド",
      width * 0.5 - imageWidth, height * 0.5,
      1,
      imageWidth, imageHeight,
      "/blog/images/FehCylPortraits/CYL_Alfred_Engage.png",
      imageWidth / 2, imageHeight / 2,
      0
    ));
    this.nodesData.push(new Node(1,
      "セリーヌ",
      width * 0.5 + imageWidth, height * 0.5,
      1,
      imageWidth, imageHeight,
      "/blog/images/FehCylPortraits/CYL_Celine_Engage.png",
      imageWidth / 2, imageHeight / 2,
      0
    ));



    this.linksData.push(new NodeEdge(
      "兄妹",
      0, 1,
      300)
    );

    const svg = d3.select("svg");

    this.link = svg.append("g").attr("id", "edges")
      .selectAll("line")
      .data(this.linksData)
      .enter()
      .append("line")
      .attr("stroke-width", 2)
      .attr("stroke", "gray");

    this.edgeLabels = svg.append("g").attr("id", "edgeLabels")
      .selectAll("text")
      .data(this.linksData)
      .enter()
      .append("text")
      .attr("font-size", fontSize)
      .style("text-anchor", "middle")
      .attr("stroke", "black")
      .text(x => x.label);

    this.node = svg.append("g").attr("id", "edgePoints")
      .selectAll("circle")
      .data(this.nodesData)
      .enter()
      .append("circle")
      .attr("r", d => d.radius)
      .attr("fill", "black")
      .attr("stroke", "black");

    this.imageFrame = svg.append("g").attr("id", "imageFrames")
      .selectAll("circle")
      .data(this.nodesData)
      .enter()
      .append("circle")
      .attr("r", d => d.width / 2)
      .attr("fill", imageFrameColor)
      .attr("stroke-width", imageFrameOuterWidth)
      .attr("stroke", imageFrameOuterColor);

    this.images = svg.append("g").attr("id", "images")
      .selectAll("image")
      .data(this.nodesData)
      .enter()
      .append("image")
      .attr("href", d => d.href)
      .attr("height", d => d.height)
      .attr("width", d => d.width)
      .attr("clip-path", d => `url(#circle-clip${d.index})`)
      .call(d3.drag()
        .on("start", (e, d) => {
          if (!e.active) this.simulation.alphaTarget(0.3).restart();
          d.fx = d.x;
          d.fy = d.y;
        })
        .on("drag", (e, d) => {
          d.fx = e.x;
          d.fy = e.y;
        })
        .on("end", (e, d) => {
          d.fx = e.x;
          d.fy = e.y;
        }));

    this.clipPaths = svg
      .select("defs")
      .selectAll("clipPath")
      .data(this.nodesData)
      .enter()
      .append("clipPath")
      .attr("id", d => "circle-clip" + d.index)
      .append("circle")
      .attr("r", d => d.width / 2 - (imageFrameInnerWidth + imageFrameOuterWidth));

    svg.select("defs")
      .selectAll("marker")
      .data(["arrow"])
      .enter()
      .append("marker")
      .attr("id", String)
      .attr("viewBox", "0 -5 10 10")
      .attr("refX", imageWidth / 2)
      .attr("refY", 0)
      .attr("markerWidth", 6)
      .attr("markerHeight", 6)
      .attr("orient", "auto-start-reverse")
      .append("svg:path")
      .attr("d", "M0,-5L10,0L0,5");

    this.nodeLabels = svg.append("g")
      .selectAll("text")
      .data(this.nodesData)
      .enter()
      .append("text")
      .attr("font-size", fontSize)
      .style("text-anchor", "middle")
      .attr("stroke", "black")
      .attr("filter", "url(#solid)")
      .text(x => x.label);


    this.simulation = d3.forceSimulation(this.nodesData)
      .on("tick", () => {
        const fontHeight = 20;
        this.link
          .attr("x1", d => this.nodesData[d.source].x)
          .attr("y1", d => this.nodesData[d.source].y)
          .attr("x2", d => this.nodesData[d.target].x)
          .attr("y2", d => this.nodesData[d.target].y)
          .attr("marker-start", "url(#arrow)")
          .attr("marker-end", "url(#arrow)");

        this.edgeLabels
          .attr("x", d => (this.nodesData[d.source].x + this.nodesData[d.target].x) / 2)
          .attr("y", d => (this.nodesData[d.source].y + this.nodesData[d.target].y) / 2 - d.labelOffset);

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

        this.images
          .attr("x", d => d.x - d.imageOffsetX)
          .attr("y", d => d.y - d.imageOffsetY);

        this.imageFrame
          .attr("cx", d => d.x)
          .attr("cy", d => d.y);

        this.nodeLabels
          .attr("x", d => d.x - d.imageOffsetX + d.width / 2)
          .attr("y", d => d.y + d.height / 2 - 5);

        this.clipPaths
          .attr("cx", d => d.x - d.imageOffsetX + d.width / 2)
          .attr("cy", d => d.y - d.imageOffsetY + d.height / 2);
      });
  }
}

const appData = new RelationshipChart();
appData.initMain();

</script>

  このエントリーをはてなブックマークに追加  

<<「Web 開発」の記事一覧に戻る

コメント(0 件)



コンテンツロード: 0.0087 sec
Copyright(C)2006-2024 puarts All Rights Reserved