function force1(data){
const width =1200;
const height = 1500;
const r = 15 ;
const w = 1;
const customColors = [
"#69b3a2",
"#f05b4f",
"#ffa500",
"#50e3c2",
"#0099cc",
"#99cc99",
"#e6e6e6",
"#66b3ff",
"#ff99cc",
"#ccff99"
];
const color = d3.scaleOrdinal(customColors);
const links = data.links.map((d) => Object.create(d));
const nodes = data.nodes.map((d) => Object.create(d));
const simulation = d3.forceSimulation(nodes)
.force("link", d3.forceLink(links).id((d) => d.id).distance(80))
.force("charge", d3.forceManyBody().strength(-300))
.force("center", d3.forceCenter(width / 2, height / 2))
.on("tick", ticked)
const svg = d3.create("svg")
.attr("height", height)
.attr("width", width)
.attr("viewBox", [0, 0, width, height])
const link = svg.append("g")
.attr("stroke", "var(--theme-foreground-faint)")
.attr("stroke-width",w)
.selectAll("line")
.data(links)
.join("line")
.attr("stroke-dasharray", function(d) {
if (d.value === "流传"||d.value === "题跋"||d.value ==="印章") {
return "10,10";
} else {
return "0";
}})
.attr("stroke-width", function(d) {
if (d.value === "流传"||d.value === "交易"||d.value ==="捐赠"||d.value ==="传承"||d.value ==="合作") {
return 2*w;
} else {
return w;
}})
const linkText = svg.append("g").selectAll("text")
.data(links)
.enter().append("text")
.attr("x", d => (d.source.x + d.target.x) / 2)
.attr("y", d => (d.source.y + d.target.y) / 2)
.text(d => d.value)
.attr("text-anchor", "middle")
.attr("alignment-baseline", "middle")
.attr("font-size", "25")
.attr("fill", "transparent")
.style("user-select", "none")
svg.on("mousemove", function(event) {
const [mouseX, mouseY] = d3.pointer(event);
linkText.attr("fill", d => isNearLink(d, mouseX, mouseY) ? "black" : "transparent");
});
const node = svg.append("g")
.attr("stroke", "var(--theme-background)")
.attr("stroke-width", 0)
.selectAll("circle")
.data(nodes)
.join("circle")
.attr("r", r)
.attr("fill", (d) => color(d.group))
.call(drag(simulation));
const nodeText = svg.append("g").selectAll("text")
.data(nodes)
.enter().append("text")
.attr("x", d => d.x)
.attr("y", d => d.y)
.text(d => d.id)
.attr("text-anchor", "middle")
.attr("alignment-baseline", "middle")
.attr("fill", "transparent")
.attr("pointer-events", "none")
.style("user-select", "none");
node.append("title")
.text((d) => d.id);
nodes.forEach((d, i) => {
d.x = i * (width / nodes.length);
d.y = height / 2;
});
function drag(simulation) {
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;
}
return d3.drag()
.on("start", dragstarted)
.on("drag", dragged)
.on("end", dragended);
}
function dragged(event) {
const xMin = 0;
const xMax = width;
const yMin = 0;
const yMax = height;
event.subject.fx = Math.max(xMin, Math.min(xMax, event.x));
event.subject.fy = Math.max(yMin, Math.min(yMax, event.y));
}
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);
linkText
.attr("x", d => (d.source.x + d.target.x) / 2)
.attr("y", d => (d.source.y + d.target.y) / 2);
nodeText
.attr("x", d => d.x)
.attr("y", d => d.y);
}
function isNearLink(link, mouseX, mouseY) {
const x1 = link.source.x;
const y1 = link.source.y;
const x2 = link.target.x;
const y2 = link.target.y;
const distance = Math.abs((y2-y1)*mouseX - (x2-x1)*mouseY + x2*y1 - y2*x1) / Math.sqrt((y2-y1)**2 + (x2-x1)**2);
return distance < 5;
}
return svg.node();
}
function Graph(data){
const width = 620;
const height = width;
const innerRadius = 130;
const outerRadius = 200;
const FCS = data.FCS.map((d) => Object.create(d));
const WYS = data.WYS.map((d) => Object.create(d));
const SST = data.SST.map((d) => Object.create(d));
const HJT = data.HJT.map((d) => Object.create(d));
const ZMJ = data.ZMJ.map((d) => Object.create(d));
const SZJ = data.SZJ.map((d) => Object.create(d));
const x = d3.scaleLinear()
.domain([1347, 2024])
.range([0, 2 * Math.PI]);
const y = d3.scalePow()
.exponent(0.8)
.domain([0, 1000])
.range([innerRadius, outerRadius]);
const lineGenerator = d3.lineRadial()
.curve(d3.curveCatmullRom)
.angle(d => x(d.year))
.radius(d => y(d.avg));
const lines = [
{data: WYS, color: "steelblue"},
{data: ZMJ, color: "yellow"},
{data: HJT, color: "red"},
{data: FCS, color: "steelblue"},
{data: SST, color: "steelblue"},
{data: SZJ, color: "green"},
];
const svg = d3.create("svg")
.attr("width", width)
.attr("height", height)
.attr("viewBox", [-width / 2, -height / 2, width, height])
.attr("style", "width: 100%; height: auto; font: 10px sans-serif;")
.attr("stroke-linejoin", "round")
.attr("stroke-linecap", "round");
const tooltip = d3.select("body")
.append("div")
.attr("class", "tooltip")
.style("position", "absolute")
.style("background", "#f9f9f9")
.style("padding", "5px")
.style("border", "1px solid #d3d3d3")
.style("border-radius", "5px")
.style("pointer-events", "none")
.style("opacity", 0);
const defs = svg.append("defs");
lines.forEach((line, index) => {
const gradientId = `gradient-${index}`;
const gradient = defs.append("radialGradient")
.attr("id", gradientId)
.attr("cx", "50%")
.attr("cy", "50%")
.attr("r", "100%")
.attr("fx", "50%")
.attr("fy", "50%");
gradient.append("stop")
.attr("offset", "35%")
.attr("stop-color", "#f9f0ea")
.attr("stop-opacity", 0.7);
gradient.append("stop")
.attr("offset", "90%")
.attr("stop-color", line.color)
.attr("stop-opacity", 0.1);
svg.append("path")
.datum(line.data)
.attr("fill", `url(#${gradientId})`)
.attr("stroke", line.color)
.attr("stroke-width", 1)
.attr("stroke-opacity", 0.5)
.attr("d", lineGenerator);
svg.append("path")
.datum(line.data)
.attr("fill", "none")
.attr("stroke", "transparent")
.attr("stroke-width", 15)
.attr("d", lineGenerator)
.on("mouseover", function(event, d) {
tooltip.transition()
.duration(100)
.style("opacity", 1);
})
.on("mousemove", (event, d) => {
const [xPos, yPos] = d3.pointer(event);
const angle = Math.atan2(yPos, xPos);
const year = Math.round(x.invert(angle));
const dataPoint = d.find(data => data.year === year);
const avgValue = dataPoint ? dataPoint.avg : null;
tooltip.html(`Year: ${year}`)
.style("left", (event.pageX + 10) + "px")
.style("top", (event.pageY - 28) + "px")
})
.on("mouseout", (d) => {
tooltip.transition()
.duration(100)
.style("opacity", 0)
});
});
svg.append("g")
.attr("text-anchor", "middle")
.attr("font-size","5")
.selectAll()
.data(y.ticks(2).reverse())
.join("g")
.call(g => g.append("circle")
.attr("fill", "none")
.attr("stroke", "currentColor")
.attr("stroke-opacity", 0.05)
.attr("r", y))
.call(g => g.append("text")
.attr("y", d => -y(d))
.text((x, i) => `${x.toFixed(0)}${i ? "" : "km"}`)
.clone(true)
.attr("y", d => y(d)))
.style("user-select","none")
.attr("z-index",100)
svg.append("g")
.selectAll()
.data(x.ticks(7))
.join("g")
.call(g => g.append("path")
.attr("stroke", "#000")
.attr("stroke-opacity", 0.05)
.attr("d", d => `
M${d3.pointRadial(x(d), innerRadius + 100)}
L${d3.pointRadial(x(d), outerRadius - 70)}
`))
svg.append("circle")
.attr("cx", 0)
.attr("cy", 0)
.attr("r", 80)
.style("fill", "#f9f0ea");
var rects = [
{ x: 270, y: 80, width: 20, height: 8, color: "blue", text: "富春山居图" },
{ x: 240, y: 120, width: 20, height: 8, color: "yellow", text: "子明卷" },
{ x: 210, y: 160, width: 20, height: 8, color: "red", text: "黄潜临本" },
{ x: 180, y: 200, width: 20, height: 8, color: "green", text: "沈周临本" }
];
rects.forEach(function(rect) {
svg.append("rect")
.attr("x", rect.x)
.attr("y", rect.y)
.attr("width", rect.width)
.attr("height", rect.height)
.style("fill", rect.color);
svg.append("text")
.attr("x", rect.x + rect.width / 2)
.attr("y", rect.y + rect.height + 10)
.attr("text-anchor", "middle")
.attr("font-size","6")
.text(rect.text);
});
return svg.node()
}