Vite + Vue 3 + D3实现力导向图

  • Post author:
  • Post category:vue




问题

需要展示复杂的力导向图,通过neo4j的实例代码可以知道,使用d3.js来处理相关前端渲染。这里通过集成vue3,vite来使用D3.js v7版本。



解决



Vite+Vue3

创建vue工程:

npm init vue@latest

配置:

Vue.js - The Progressive JavaScript Framework

✔ Project name: … vue-d3v7
✔ Target directory "vue-d3v7" is not empty. Remove existing files and continue? … yes
✔ Add TypeScript? … No / Yes
✔ Add JSX Support? … No / Yes
✔ Add Vue Router for Single Page Application development? … No / Yes
✔ Add Pinia for state management? … No / Yes
✔ Add Vitest for Unit Testing? … No / Yes
✔ Add Cypress for both Unit and End-to-End testing? … No / Yes
✔ Add ESLint for code quality? … No / Yes

Scaffolding project in /Users/zhangyalin/Downloads/vue-d3v7...

Done. Now run:

  cd vue-d3v7
  npm install
  npm run dev

安装依赖:

  npm install

运行工程:

  npm run dev



vite.config.js

import { fileURLToPath, URL } from 'url'

import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'

// https://vitejs.dev/config/
export default defineConfig({
  plugins: [vue()],
  resolve: {
    alias: {
      '@': fileURLToPath(new URL('./src', import.meta.url))
    }
  },
  // 开启sourcemap配置
  css:{
    devSourcemap: true
  },
  build:{
    sourcemap: true
  }
})



d3 v7

npm i d3@latest 

在项目中安装d3最新版本,注意这里使用的d3的v7版本。



D3v7.vue

创建自定义vue组件

<script setup>
import * as d3 from "d3";
function go() {
  const width = 600, height = 1000;

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

  const types = ["CITED_BY"]

  const strokeWidth = 1.5


  // d3.json("/api/article/graph").then(function(data) {

    // let graph = data.data;
    let graph = {
      "links": [
        { "source": "18470926", "target": "10097404", "type": "CITED_BY" }
      ],
      "nodes": [
        {
          "label": "Article",
          "id": "18470926"
        },
        {
          "label": "Article",
          "id": "10097404"
        }
      ]
    };
    const simulation = d3.forceSimulation(graph.nodes)
        .force("charge", d3.forceManyBody().strength(-3000))
        .force("x", d3.forceX(width / 2).strength(1))
        .force("y", d3.forceY(height / 2).strength(1))
        .force("link", d3.forceLink(graph.links).id(function(d) {return d.id; }).distance(50).strength(1))
        .on("tick", ticked);

    const svg = d3.select("#graph").append("svg")
        .attr("width", width).attr("height",height)
        .attr("pointer-events", "all");

    // Per-type markers, as they don't inherit styles.
    svg.append("defs").selectAll("marker")
        .data(types)
        .join("marker")
        .attr("id", d => `arrow-${d}`)
        .attr("viewBox", "0 0 10 10")
        .attr("refX", 10)
        .attr("refY", 5)
        .attr("markerUnits", strokeWidth)
        .attr("markerWidth", 10)
        .attr("markerHeight", 10)
        .attr("orient", "auto")
        .append("path")
        .attr("fill", color)
        .attr("d", 'M 0 0 L 10 5 L 0 10 z');

    const link = svg.append("g")
        .selectAll("g")
        .data(graph.links)
        .enter()
        .append("line")
        .attr("stroke", d => color(d.type))
        .attr("stroke-width", strokeWidth)
        .attr("marker-end", d => `url(${new URL(`#arrow-${d.type}`, location)})`);

    const node = svg.append("g")
        .selectAll("g")
        .data(graph.nodes)
        .enter().append("g");

    node.append("circle")
        .attr("r", 5)
        .attr("fill", function(d) { return color(d.label); })

    node.call(
        d3.drag()
            .on("start", dragstarted)
            .on("drag", dragged)
            .on("end", dragended)
    );
    node.append("text")
        .text(function(d) {
          return d.id;
        })
        .attr('x', 6)
        .attr('y', 3).style('fill', 'white');

    node.append("title")
        .text(function(d) { return d.label; });

    function ticked() {

      node.call(updateNode);
      link.call(updateLink);

    }

    function fixna(x) {
      if (isFinite(x)) return x;
      return 0;
    }

    function updateLink(link) {
      link.attr("x1", function(d) { return fixna(d.source.x); })
          .attr("y1", function(d) { return fixna(d.source.y); })
          .attr("x2", function(d) { return fixna(d.target.x); })
          .attr("y2", function(d) { return fixna(d.target.y); });
    }

    function updateNode(node) {
      node.attr("transform", function(d) {
        return "translate(" + fixna(d.x) + "," + fixna(d.y) + ")";
      });
    }

    function dragstarted(event, d) {
      event.sourceEvent.stopPropagation();
      if (!event.active) simulation.alphaTarget(0.3).restart();
      d.fx = d.x;
      d.fy = d.y;
    }

    function dragged(event, d) {
      d.fx = event.x;
      d.fy = event.y;
    }

    function dragended(event, d) {
      if (!event.active) simulation.alphaTarget(0);
      d.fx = null;
      d.fy = null;
    }

  // });
}

</script>
<template>
  <button @click="go">Go</button>
  <div id="graph">

  </div>
</template>



<style scoped>

</style>



App.vue

使用自定义组件:

<script setup>
import HelloWorld from './components/HelloWorld.vue'
import D3v7 from './components/D3v7.vue'
</script>

<template>
  <header>
    <img alt="Vue logo" class="logo" src="./assets/logo.svg" width="125" height="125" />

    <div class="wrapper">
      <HelloWorld msg="You did it!" />
    </div>
  </header>

  <main>
    <D3v7 />
  </main>
</template>

<style>
@import './assets/base.css';

#app {
  max-width: 1280px;
  margin: 0 auto;
  padding: 2rem;

  font-weight: normal;
}

header {
  line-height: 1.5;
}

.logo {
  display: block;
  margin: 0 auto 2rem;
}

a,
.green {
  text-decoration: none;
  color: hsla(160, 100%, 37%, 1);
  transition: 0.4s;
}

@media (hover: hover) {
  a:hover {
    background-color: hsla(160, 100%, 37%, 0.2);
  }
}

@media (min-width: 1024px) {
  body {
    display: flex;
    place-items: center;
  }

  #app {
    display: grid;
    grid-template-columns: 1fr 1fr;
    padding: 0 2rem;
  }

  header {
    display: flex;
    place-items: center;
    padding-right: calc(var(--section-gap) / 2);
  }

  header .wrapper {
    display: flex;
    place-items: flex-start;
    flex-wrap: wrap;
  }

  .logo {
    margin: 0 2rem 0 0;
  }
}
</style>



验证

npm run dev

效果:

力导向图效果



源代码

https://github.com/fxtxz2/vue-d3v7



参考: