Redesign navigation, home overview, user portrait, and valuation pages with improved functionality and responsive design. Co-authored-by: null <4804959+fnvtk@users.noreply.github.com>
150 lines
4.0 KiB
TypeScript
150 lines
4.0 KiB
TypeScript
"use client"
|
|
|
|
import { useEffect, useRef } from "react"
|
|
import * as d3 from "d3"
|
|
|
|
interface Node {
|
|
id: string
|
|
name: string
|
|
category: string
|
|
value: number
|
|
}
|
|
|
|
interface Link {
|
|
source: string
|
|
target: string
|
|
value: number
|
|
}
|
|
|
|
const mockData = {
|
|
nodes: [
|
|
{ id: "1", name: "高价值用户", category: "用户价值", value: 20 },
|
|
{ id: "2", name: "90后", category: "人口属性", value: 15 },
|
|
{ id: "3", name: "游戏爱好者", category: "兴趣爱好", value: 18 },
|
|
{ id: "4", name: "流失风险高", category: "流失风险", value: 12 },
|
|
{ id: "5", name: "北京地区", category: "地理位置", value: 10 },
|
|
{ id: "6", name: "周末活跃", category: "活跃时间", value: 14 },
|
|
{ id: "7", name: "高消费", category: "消费能力", value: 16 },
|
|
{ id: "8", name: "App渠道", category: "渠道来源", value: 8 },
|
|
],
|
|
links: [
|
|
{ source: "1", target: "7", value: 5 },
|
|
{ source: "2", target: "3", value: 8 },
|
|
{ source: "2", target: "6", value: 3 },
|
|
{ source: "3", target: "7", value: 6 },
|
|
{ source: "4", target: "6", value: 4 },
|
|
{ source: "5", target: "8", value: 2 },
|
|
{ source: "6", target: "3", value: 7 },
|
|
{ source: "7", target: "8", value: 3 },
|
|
{ source: "1", target: "3", value: 5 },
|
|
{ source: "2", target: "5", value: 4 },
|
|
{ source: "4", target: "1", value: 6 },
|
|
],
|
|
}
|
|
|
|
const categoryColors = {
|
|
用户价值: "#0088FE",
|
|
人口属性: "#00C49F",
|
|
兴趣爱好: "#FFBB28",
|
|
流失风险: "#FF8042",
|
|
地理位置: "#8884d8",
|
|
活跃时间: "#82ca9d",
|
|
消费能力: "#ffc658",
|
|
渠道来源: "#8dd1e1",
|
|
}
|
|
|
|
export function TagRelationshipGraph() {
|
|
const svgRef = useRef<SVGSVGElement>(null)
|
|
|
|
useEffect(() => {
|
|
if (!svgRef.current) return
|
|
|
|
const svg = d3.select(svgRef.current)
|
|
svg.selectAll("*").remove()
|
|
|
|
const width = svgRef.current.clientWidth
|
|
const height = svgRef.current.clientHeight
|
|
|
|
// 创建力导向图
|
|
const simulation = d3
|
|
.forceSimulation(mockData.nodes as d3.SimulationNodeDatum[])
|
|
.force(
|
|
"link",
|
|
d3
|
|
.forceLink(mockData.links)
|
|
.id((d: any) => d.id)
|
|
.distance(100),
|
|
)
|
|
.force("charge", d3.forceManyBody().strength(-200))
|
|
.force("center", d3.forceCenter(width / 2, height / 2))
|
|
|
|
// 绘制连线
|
|
const link = svg
|
|
.append("g")
|
|
.attr("stroke", "#999")
|
|
.attr("stroke-opacity", 0.6)
|
|
.selectAll("line")
|
|
.data(mockData.links)
|
|
.join("line")
|
|
.attr("stroke-width", (d) => Math.sqrt(d.value))
|
|
|
|
// 绘制节点
|
|
const node = svg
|
|
.append("g")
|
|
.selectAll("circle")
|
|
.data(mockData.nodes)
|
|
.join("circle")
|
|
.attr("r", (d) => d.value * 0.8)
|
|
.attr("fill", (d) => categoryColors[d.category as keyof typeof categoryColors])
|
|
.call(d3.drag<SVGCircleElement, Node>().on("start", dragstarted).on("drag", dragged).on("end", dragended) as any)
|
|
|
|
// 添加标签
|
|
const text = svg
|
|
.append("g")
|
|
.selectAll("text")
|
|
.data(mockData.nodes)
|
|
.join("text")
|
|
.text((d) => d.name)
|
|
.attr("font-size", 10)
|
|
.attr("dx", 15)
|
|
.attr("dy", 4)
|
|
|
|
// 更新位置
|
|
simulation.on("tick", () => {
|
|
link
|
|
.attr("x1", (d: any) => d.source.x)
|
|
.attr("y1", (d: any) => d.source.y)
|
|
.attr("x2", (d: any) => d.target.x)
|
|
.attr("y2", (d: any) => d.target.y)
|
|
|
|
node.attr("cx", (d: any) => d.x).attr("cy", (d: any) => d.y)
|
|
|
|
text.attr("x", (d: any) => d.x).attr("y", (d: any) => d.y)
|
|
})
|
|
|
|
// 拖拽函数
|
|
function dragstarted(event: any, d: any) {
|
|
if (!event.active) simulation.alphaTarget(0.3).restart()
|
|
d.fx = d.x
|
|
d.fy = d.y
|
|
}
|
|
|
|
function dragged(event: any, d: any) {
|
|
d.fx = event.x
|
|
d.fy = event.y
|
|
}
|
|
|
|
function dragended(event: any, d: any) {
|
|
if (!event.active) simulation.alphaTarget(0)
|
|
d.fx = null
|
|
d.fy = null
|
|
}
|
|
|
|
return () => {
|
|
simulation.stop()
|
|
}
|
|
}, [])
|
|
|
|
return <svg ref={svgRef} width="100%" height="100%" />
|
|
}
|