Published
Edited
Mar 8, 2021
Fork of Arc Diagram
Insert cell
md`# Arc Diagram
An arc diagram is a special kind of network graph. It is consituted by nodes that represent entities and by links that show relationships between entities. In arc diagrams, nodes are displayed along a single axis and links are represented with arcs.`
Insert cell
chart = {
const svg = d3.create("svg")
.attr("font-size", "11pt")
.attr("viewBox", [0, 0, width, height]);
// 箭头相关
const markerSize = 5;
const markerBoxWidth = markerSize;
const markerBoxHeight = markerSize;
const refX = markerBoxWidth / 2;
const refY = markerBoxHeight / 2;
const markerWidth = markerBoxWidth / 2;
const markerHeight = markerBoxHeight / 2;
const arrowPoints = [[0, 0], [0, markerSize], [markerSize, markerSize / 2]];
var defs = svg.append("defs");
//渐变色1,为圆弧的渐变色
var linerGradient = defs.append("linearGradient")
.attr("id","linearColor")
//.attr("gradientUnits","userSpaceOnUse")
.attr("x1","0%")
.attr("y1","0%")
.attr("x2","100%")
.attr("y2","0%");
var stop1 = linerGradient.append("stop")
.attr("offset","0%")
.attr("stop-color","#b34689")
.attr("stop-opacity",0.75);
var stop2 = linerGradient.append("stop")
.attr("offset","100%")
.attr("stop-color","#b34689")
.attr("stop-opacity",0);

//渐变色2,为年份直线的渐变色
var linerGradient2 = defs.append("linearGradient")
.attr("id","linearColor2")
.attr("gradientUnits","userSpaceOnUse")
.attr("x1","100%")
.attr("y1","50%")
.attr("x2","100%")
.attr("y2","0%");
var stop2_1 = linerGradient2.append("stop")
.attr("offset","0%")
.attr("stop-color","#fff")
.attr("stop-opacity",0.75);
var stop2_2 = linerGradient2.append("stop")
.attr("offset","100%")
.attr("stop-color","#000")
.attr("stop-opacity",0);
var arrowMarker = defs
.append('marker')
.attr("markerUnits", "strokeWidth")
.attr('markerWidth', 6)
.attr('markerHeight', 6)
.attr('id', 'arrow')
.attr('viewBox', "0, -5, 10, 10")
.attr('refX', '8')
.attr('refY', '2')
.attr('orient', 'auto')
.append('path')
.attr('d', d3.line()(arrowPoints))
.attr('fill', "#b34689");

const arcs = svg.append('g')//arcs:由结点出发的圆弧
.selectAll("path")
.data(graph.links)
.join("path")
.attr("fill", "none")
.attr("stroke", "#ccc")
.attr('stroke-opacity', 0)
.attr("stroke-width", d => 2)
.attr("d", arc)
.on("mouseover", highlight)
.on("mouseout", restore);
const arcs2 = svg.append('g')//arcs2:指向结点的圆弧
.selectAll("path")
.data(graph.links)
.join("path")
.attr("fill", "none")
.attr("stroke", "#ccc")
.attr('stroke-opacity', 0)
.attr("stroke-width", d => 2)
.attr("d", arc2)
.on("mouseover", highlight)
.on("mouseout", restore);

//年份的直线
for(var i=0;i<graph.nodes.length-1;i++){
var currentYear=graph.nodes[i].year;
var nextYear = graph.nodes[i+1].year;
let scale = d3.scaleLinear().domain([currentYear,nextYear]).range([xScale(graph.nodes[i].idx),xScale(graph.nodes[i+1].idx)]);
for(var j=currentYear;j<=nextYear;j++){
var positionX=scale(j);
svg.append("line")
.attr("x1",function(d){
return positionX;
})
.attr("y1",baseHeight)
.attr("x2",function(d){
return positionX;
})
.attr("y2",50)
.attr("stroke","#000")
.attr("stroke","url(#"+linerGradient2.attr("id")+")")
.attr("stroke-width",0.5);
}
}
//年份标注
for(var i=0;i<graph.nodes.length-1;i++){
var currentYear=graph.nodes[i].year;
var nextYear = graph.nodes[i+1].year;
let scale = d3.scaleLinear().domain([currentYear,nextYear]).range([xScale(graph.nodes[i].idx),xScale(graph.nodes[i+1].idx)]);
for(var j=currentYear;j<nextYear;j++){
var positionX=scale(j);
svg.append("g")
.append("text")
.attr("text-anchor", "start")
.attr('font-size', 7)
.attr('font-family', 'sans-serif')
.style('fill',function(d){
if(j%5==0&&j>1940){
console.log("year"+j);
return "#000";
}
else return 'none';
})
.attr("x",positionX-8)
.attr("y",55)
.text(j);
}
}
const circles = svg.selectAll(".node")
.data(graph.nodes)
.join("g")
.attr("class", "node")
.attr("transform", d => `translate(${xScale(d.idx)},${baseHeight})`)
.attr("fill", d=> color(d.name))

circles.append("circle")
.attr("r", d => rScale(d.influence))
.on("mouseover", highlight)
.on("mouseout", restore);
//text背景色
// var backgroundColor = circles.append("rect")
// .attr("width",12)
// .attr("height",function(d){
// var l = d.name.length;
// console.log(d.name.length);
// return l*6;
// })
// .attr("transform", d => `translate(-3,${rScale(d.influence) + 10}) rotate(-45)`)
// .attr("fill","#fff")

//text
var circleText = circles.append("g")
.attr("text-anchor", "start")
.attr('font-size', 10)
.attr('font-family', 'sans-serif')
.attr("transform", d => `translate(0,${rScale(d.influence) + 10}) rotate(45)`)
.call(g => g.append("text").text(d => d.name))
return svg.node();
function highlight(e, d, restore) {
circles.filter(c => c.id===d.id)
.attr("fill", c => restore ? "#ccc" : "#fff")
.attr("stroke", c => restore ? "none" : "#b34689")
.attr("stroke-width", 1.5);
if (d.name) {
arcs.filter(a => a.source === d)//arcs:由结点出发的圆弧
.attr('marker-end', 'url(#arrow)')
.attr('stroke-opacity', d=> restore? 0 : 1)
.attr("stroke", d => restore ? "#ccc" : "#b34689")
.attr("stroke-width", 2);
arcs2.filter(a => a.target === d)//arcs2:指向结点的圆弧
.attr('marker-end', 'none')
.attr('stroke-opacity', d=> restore? 0 : 1)
.attr("stroke","url(#"+linerGradient.attr("id")+")")
.attr("stroke-width",7);
circles.filter(c => hasRelation(c.id, d.id));
circles.filter(c => !hasRelation(c.id, d.id))
.attr("fill", c => restore ? color(c.name) : "#ccc")
.attr('fill-opacity', c => restore ? 1 : 0.5);
}
}
function restore(e, d) {
highlight(e, d, true);
arcs.attr('marker-end', 'none');
arcs2.attr('marker-end', 'none');
}
}
Insert cell
xScale = d3.scaleLinear()
.domain(d3.extent(graph.nodes.map(n => n.idx)))
.range([50, width - 50])
Insert cell
rScale = d3.scaleLinear()
.domain(d3.extent(graph.nodes.map(n => n.influence)))
.range([radius.min, radius.max])
Insert cell
x = d3.scalePoint()
.domain(nodes.map(d => d.name))
.range([radius.max * 2, width - radius.max * 2])
Insert cell
r = d3.scaleLinear()
.domain(d3.extent(nodes.map(d => d.total)))
.range([radius.min, radius.max]);
Insert cell
w = d3.scaleLinear()
.domain(d3.extent(links.map(d => d.value)))
.range([1, 10])
Insert cell
color = d3.scaleOrdinal()
.domain(nodes.map(d => d.name))
.range(["#4e79a7","#f28e2c","#e15759","#76b7b2","#59a14f","#edc949","#af7aa1","#ff9da7","#9c755f","#bab0ab"]);
Insert cell
arc = (d) => {//arc:由结点出发的圆弧,画在上半部分
const x1 = xScale(d.source.idx),
x2 = xScale(d.target.idx);
const r = Math.abs(x2 - x1) / 2;
return `M${x1} ${baseHeight} A ${r},${r} 0 0,${x1 < x2 ? 1 : 0} ${x2},${baseHeight}`;
}
Insert cell
arc2 = (d) => {//arc2:指向结点的圆弧,画在下半部分
const x1 = xScale(d.source.idx),
x2 = xScale(d.target.idx);
const r = Math.abs(x2 - x1) / 2;
return `M${x1/*Math.min(x1, x2)*/} ${baseHeight} A ${r},${r} 0 0,${x1 < x2 ? 0 : 1} ${x2/*Math.max(x1, x2)*/},${baseHeight}`;
}
Insert cell
toCurrency = (num) => Intl.NumberFormat('en-US', {style: 'currency', currency: 'USD'}).format(num)
Insert cell
data = d3.csvParse(await FileAttachment("profit4yr.csv").text(), d3.autoType)
Insert cell
chartData = {
return data.map(d => {
return {
territory: d["territory"],
values: data.columns.slice(1).map(y => d[y])
}
});
}
Insert cell
territories = chartData.map(d => ({
name: d.territory,
total: d.values.reduce((a, b) => a + b)
}));
Insert cell
years = data.columns.slice(1).map((d, i) => ({
name: d,
total: chartData.reduce((a, b) => a + b.values[i], 0)
}));
Insert cell
nodes = years.concat(territories)
Insert cell
Insert cell
linkedNodes = n => {
return Array.from(new Set(
links
.flatMap(d => d.source === n || d.target === n ? [d.source, d.target] : null)
.filter(d => d !== null)
));
}
Insert cell
height = 768
Insert cell
margin = radius.max * 2 + 20
Insert cell
radius = ({min: 5, max: 30})
Insert cell
baseHeight = 400
Insert cell
hasRelation = {
const relation = {}
graph.links.forEach(l => {
if (!relation[l.source.id]) {
relation[l.source.id] = new Set([l.source.id])
}
relation[l.source.id].add(l.target.id)
if (!relation[l.target.id]) {
relation[l.target.id] = new Set()
}
relation[l.target.id].add(l.source.id)
})
return function check(a, b) {
return relation[a] && relation[a].has(b)
}
}
Insert cell
graph = {
const nodes = {}
musicianData.artists.forEach(n => nodes[n.id] = {id: n.id, name: n.artist, year: n.years[0].start, influence: 0})
musicianData.influencers.edges.forEach(l => nodes[l.source].influence += 1)
const nodeArr = Object.values(nodes).sort((a, b) => a.year - b.year)
nodeArr.forEach((x, i) => x.idx = i)
const links = musicianData.influencers.edges.map(e => ({source: nodes[e.source], target: nodes[e.target]}))
const data = {
nodes: nodeArr,
links: links
}
return data
}
Insert cell
Insert cell
d3 = require("d3@6")
Insert cell
Insert cell

Purpose-built for displays of data

Observable is your go-to platform for exploring data and creating expressive data visualizations. Use reactive JavaScript notebooks for prototyping and a collaborative canvas for visual data exploration and dashboard creation.
Learn more