Published
Edited
Dec 13, 2021
Insert cell
md`# 小组会议发言及回复关系2`
Insert cell
{
const paddingTop = 100
const svg = d3.create("svg")
.attr("width",width)
.attr("height",height)
.attr("viewBox", [0, 0, width*2, height])
.style("font", "10px sans-serif");
const g = svg.append("g")
.attr("transform",`translate(0,${paddingTop})`)
g.append("g")
.call(axisRight)
g.append("g")
.call(axisBottom)
const storyBoard = g.append("g")
.attr("class","storyBoard")
/* 未捆绑发言节点 */
const nodes = storyBoard.selectAll("circle")
.data(circles)
.join("circle")
.attr("class",(d,i) => `detailSid_${bind_edges.idx2sessionId[i] ? bind_edges.idx2sessionId[i] : 'single'}`)
.attr("r",d=>d.r)
.attr("cx",d=>d.x)
.attr("cy",d=>d.y)
.attr("fill","grey")
.attr("opacity",(d,i)=>{
return bind_edges.idx2sessionId[i] ? 0 : 0.7
})

// 回复线渐变颜色
const defs = g.append("defs")
const linearGradients = defs
.selectAll("linearGradient")
.data(edges.filter(v=>v.source !== '-'))
.join("linearGradient")
.attr("id",(d,i)=>`BuReGradient${i}`)
.attr("gradientUnits","userSpaceOnUse") //使用真实坐标定义起点终点
.attr("x1",d=>idx2pos(d.source)[0])
.attr("y1",d=>idx2pos(d.source)[1])
.attr("x2",d=>idx2pos(d.target)[0])
.attr("y2",d=>idx2pos(d.target)[1])
linearGradients.append("stop")
.attr("offset","0%")
.attr("stop-color",d3.interpolateCividis(0))
// linearGradients.append("stop")
// .attr("offset","50%")
// .attr("stop-color",d3.interpolateCividis(0.5))
linearGradients.append("stop")
.attr("offset","100%")
.attr("stop-color",d3.interpolateCividis(1))
/* 未捆绑回复线 */
g.append("g")
.selectAll("path")
.data(edges.filter(v=>v.source !== '-'))
.join("path")
.attr("d",d => {
return dialogs[d.source].role !== dialogs[d.target].role ?
lineCurveTo(idx2pos(d.source),idx2pos(d.target)) : arcTo(d)
}
)
.attr("stroke",(d,i)=>`url(#BuReGradient${i})`)
.attr("opacity",0)
.attr("stroke-width","3")
.attr("stroke-linecap","round")
.attr("fill","none")
.attr("class",d =>{
return `detailSid_${bind_edges.idx2sessionId[d.source] ? bind_edges.idx2sessionId[d.source] : 'single'}`
})

/* 捆绑回复线 */
g.append("g")
.selectAll("path")
.data(bind_edges.edges)
.join("path")
.attr("d", d => CubeBezierTo(d))
.attr("stroke",(d,i)=>d3.color((d3.interpolateSpectral(Math.random(i)))).darker())
.attr("fill","none")
.attr("stroke-width",d => d.weight)
.attr("stroke-opacity","1")
.attr("class",d => {
if(d.type === "trunk")
return `sid_${d.id.split("_")[0]}`
else
return `sid_${bind_edges.idx2sessionId[d.id]}`
})
/* 捆绑子会话发言节点 */
g.append("g")
.selectAll("rect")
.data(bind_edges.squares)
.join("rect")
.attr("x",d => d['start'])
.attr("y",d => yScale(d['role']) - 10)
.attr("height",20)
.attr("width",d => d['end']-d['start'])
.attr("fill-opacity","0.8")
.attr("rx",10)
.attr("ry",10)
.attr("fill","grey")
.attr("class",d => `sid_${d["session_id"]}`)
.on("click",click)

return svg.node()

}
Insert cell
md`## Function`
Insert cell
click = function(e,d){
console.log(this,e,d)
d3.selectAll(`.sid_${d.session_id}`).attr("opacity","0")
d3.selectAll(`.detailSid_${d.session_id}`).attr("opacity",0.7)
}
Insert cell
function arcTo(d) { // 回复关系圆弧
const x1 = xScale((Number(dialogs[d.source].startTime) + Number(dialogs[d.source].endTime))/2)
const x2 = xScale((Number(dialogs[d.target].startTime) + Number(dialogs[d.target].endTime))/2)
const y1 = yScale(dialogs[d.source].role)
const y2 = yScale(dialogs[d.target].role)
const r = Math.abs(x2 - x1) / 2;
return `M${x1},${y1}A${r},${r/4} 0,0,${x1 < x2 ? 1 : 0} ${x2},${y2}`;
}
Insert cell
lineTo = (a,b) => {
const p = d3.path()
p.moveTo(...a)
p.lineTo(b[0],b[1])
return p.toString()
}
Insert cell
lineCurveTo = (a,b) => {
const p = d3.path()
p.moveTo(...a)
const distance = 75
if(a[1] < b[1])
p.bezierCurveTo(a[0],a[1]+distance,b[0],b[1]-distance,...b)
else
p.bezierCurveTo(a[0],a[1]-distance,b[0],b[1]+distance,...b)
return p.toString()
}
Insert cell
lineClose = d3.line().curve(d3.curveBasisClosed)
Insert cell
xScale = d3.scaleLinear()
.domain([0,dialogs[dialogs.length-1].endTime+10])
.range([0+padding,width-padding])
Insert cell
yScale = d3.scalePoint()
.domain(roles)
.range([0+padding,width/2-padding])
.round(true)
.padding(0.5)
Insert cell
yScaleBar = d3.scaleLinear()
.domain([0,40])
.range([0,-100])
Insert cell
formatSecond = value => value+"s"
Insert cell
format = (value)=>{
let min = Math.floor(value/60)
let second = value % 60
return "" + min + "min"+second+"s"
}
Insert cell
axisBottom = svg => svg.call(d3.axisBottom(xScale)
.tickFormat(formatSecond)
.tickSizeOuter(0))
.call(g => g.select(".domain").remove())
.call(g => g.attr("font-size",25))

Insert cell
axisRight = svg => svg.call(d3
.axisRight(yScale)
.tickSize(width)
.tickSizeOuter(0)
)
.call(g => g.selectAll(".tick line")
.attr("stroke-opacity",0.5)
.attr("stroke-dasharray","2,2"))
.call(g => g.selectAll(".tick text")
.attr("x",4)
.attr("dy",-4))
Insert cell
arc = d3.arc()
.startAngle(d => d.startAngle)
.endAngle(d => d.endAngle)
.innerRadius(0)

Insert cell
idx2pos = (idx)=>{
const x = xScale((Number(dialogs[idx].startTime) + Number(dialogs[idx].endTime))/2)
const y = yScale(dialogs[idx].role)
return [x,y]
}
Insert cell
CubeBezierTo = d => {
const source = d.source
const target = d.target
const mid_y = (d.source[1] + d.target[1])/2
const v_source = [d.source[0],mid_y]
const v_target = [d.target[0],mid_y]
const p = d3.path()
p.moveTo(...source)
p.bezierCurveTo(...v_source,...v_target,...target)
return p.toString()
}
Insert cell
md`## Configuration`
Insert cell
color = d3.schemeCategory10
Insert cell
width = 450 * 4
Insert cell
height = width
Insert cell
topicNum = 3
Insert cell
padding = 20
Insert cell
md`## Data`
Insert cell
//dialogs = FileAttachment("ES2002all(3btm)@1.json").json()
Insert cell
//dialogs =FileAttachment("ES2002a(dt)(3btm).json").json()
Insert cell
dialogs_all = {
let d = FileAttachment("ES2002a.json").json()
return d
}
Insert cell
dialogs = dialogs_all
Insert cell
reply_relation_all = FileAttachment("reply_ES2002a@2.json").json()
Insert cell
reply_relation = reply_relation_all
Insert cell
edges = reply_relation.map(v=>{
return {
source:v.reply_to_id === '-' ? '-': Number(v.reply_to_id),
target:v.id
}
})
Insert cell
sessions = FileAttachment("edgeBunding_ES2002a.json").json()
Insert cell
sessions.forEach( v => { //刷新网页只运行一次 报错是正常的
if(Object.keys(v).length === 1)
return
else{
for(let key of Object.keys(v))
{
if(key !== 'session_id')
{
let speakers = key.split("_")
if(speakers[0] === speakers[1]) //A_A
continue
let xsum = 0
for(let x of v[key])
{
xsum += idx2pos(x)[0]
}
let l = v[key]
let y
if(yScale(speakers[0]) < yScale(speakers[1]))
{
y = yScale(speakers[0]) + 30 //
}
else
{
y = yScale(speakers[0]) - 30
}
v[key] = {'pos':l,'vp':[xsum/l.length,y]} // points , virtual point
}
}
}
})
Insert cell
bind_edges = {
const edges = []
const squares = []
const idx2sessionId = {}
for(let session of sessions)
{
const speakers_dic = Object.create(null)
if(Object.keys(session).length === 1)
continue
const visited_subsession_trunk = new Set()
for(let subsession_name of Object.keys(session))
{
if( subsession_name === 'session_id' )
continue
const speakers = subsession_name.split("_")
if(speakers[0] === speakers[1])
continue
for(let id of session[subsession_name]['pos'])
{
// idx2sessionId
idx2sessionId[id] = session['session_id']
//edge
const source = idx2pos(id)
const target = session[subsession_name]['vp']
const weight = 1
const type = "branch"
const edge = {source,target,weight,type,"id":id}
edges.push(edge)

//square
const x_start = xScale(dialogs[id].startTime)
const x_end = xScale(dialogs[id].endTime)
if(!(speakers[0] in speakers_dic))
{
speakers_dic[speakers[0]] = [x_start,x_end]
}
else if(speakers_dic[speakers[0]][0] > x_start) // x_start
{
speakers_dic[speakers[0]][0] = x_start
}
else if(speakers_dic[speakers[0]][1] < x_end) // x_end
{
speakers_dic[speakers[0]][1] = x_end
}
}
if(!visited_subsession_trunk.has(subsession_name))
{
const another_vp = session[`${speakers[1]}_${speakers[0]}`]['vp']
const edge = {
source:session[subsession_name]['vp'],
target:another_vp,
weight:session[subsession_name]['pos'].length * 5,
type:"trunk",
id:session['session_id']+'_'+subsession_name
}
visited_subsession_trunk.add(subsession_name)
visited_subsession_trunk.add(`${speakers[1]}_${speakers[0]}`)
edges.push(edge)
}
}
// create session square
for(let [key,pos] of Object.entries(speakers_dic))
{
const role = key
const start = pos[0]
const end = pos[1]
const square = {role,start,end,session_id:session['session_id']}
squares.push(square)
}
}
return {edges,squares,idx2sessionId}
}
Insert cell
circles = dialogs.map(v=>{
let c = {}
let start = xScale(v.startTime)
let end = xScale(v.endTime)
// if(v.topic || v.topic === 0){
// c.arcs = d3.pie().sort(null)(v.dis)//sort(null)意思是不排序,数据按照输入的顺序排列 dis是主题分布
// }
// else
// {
// c.arcs = d3.pie()([2])
// }
// if(v.agenda != undefined)
// c.agenda = v.agenda
//c.r = (Math.floor((end-start)/2)+4) 给最小发言一定大小
c.r = (end-start)/2// 直径为实际发言时长
// c.arcs.forEach(a=>a.outerRadius = c.r)
c.x = start+c.r //圆心在起止时间中间
c.y = yScale(v.role)
return c
})
Insert cell
roles = {
return [...new Set(dialogs.map(v=>v.role))]
}
Insert cell
md`## Reference`
Insert cell
d3 = require("d3@6")
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