Published
Edited
Sep 7, 2021
Insert cell
md`# 个人发言特征图`
Insert cell
data = FileAttachment("ES2002a.json").json()
Insert cell
dts = {
let dic = {"Backchannel":0,
"Stall":0,
"Fragment":0,
"Inform":0,
"Suggest":0,
"Assess":0,
"Elicit-Inform":0,
"Elicit-Offer-Or-Suggestion":0,
"Elicit-Assessment":0,
"Elicit-Comment-Understanding":0,
"Offer":0,
"Comment-About-Understanding":0,
"Be-Positive":0,
"Be-Negative":0,
"Other":0
}
data.forEach(v=>{
if(v.role === role)
dic[v.da] ? dic[v.da]++ : dic[v.da] = 1
})
let arr = []
for(let i in dic)
{
arr.push({name:i,val:dic[i]})
}
return arr
}
Insert cell
color = d3
.scaleOrdinal()
.domain(d3.range(dts.length))
.range(
d3
.quantize(t => d3.interpolateSpectral(t * 0.8 + 0.1), dts.length)
.reverse()
)
Insert cell
height = width
Insert cell
innerRadius = 25
Insert cell
wordRadius = Math.min(width,height) * 0.5
Insert cell
outerRadius = Math.min(width, height) * 0.2
Insert cell
d3 = require("d3@6")
Insert cell
x.step()
Insert cell
x.paddingOuter()
Insert cell
x = {
let x = d3.scaleBand()
.domain(dts.map(d => d.name))
.range([0, 2 * Math.PI])
.paddingInner(0.5)
x = x.paddingOuter(x.paddingInner() * x.step())//https://observablehq.com/@d3/d3-scaleband
return x
}
Insert cell
y = d3.scaleRadial()
.domain([0, d3.max(dts, d => d.val)])
.range([innerRadius, outerRadius])
Insert cell
arc = d3.arc()
.innerRadius(d => y(0))
.outerRadius(d => y(d.val))
.startAngle(d => x(d.name))
.endAngle(d => x(d.name) + x.bandwidth())
.padAngle(0.01)
.padRadius(innerRadius)
Insert cell
Task = [['design new', 0.3253], ['favourite animal', 0.1192], ['does dinner', 0.1856], ['project manager', 0.2712], ['thirty minutes', 0.1592], ['kick meeting', 0.2261],['selling remote', 0.1273], ['know laura', 0.1429], ['better meeting', 0.1578], ['quickly couple', 0.2169],['selling international', 0.1313], ['dog home', 0.1448]]

Insert cell
words = new Array(24).fill("aaaaaaaaaaaaaaaaaaaaaa")
Insert cell
firstScaleX =
{
const closure = () => {
const scale = d3.scaleBand()
.domain(new Array(6).fill(0).map((d,i)=>i))
.range([0,Math.PI*2/3]) // 120deg
const f = (index) =>scale(index % 6)
Object.setPrototypeOf(f,scale)
return f
}
return closure()
}
Insert cell
ScaleY = index => {
const wordPadding = 2
const short = outerRadius + Math.floor(index/3) * sliceLen + wordPadding
const long = short + sliceLen - wordPadding
return {short,long}
}
Insert cell
firstScaleY = index => {
const wordPadding = 2
const short = outerRadius + Math.floor(index/6) * sliceLen + wordPadding
const long = short + sliceLen - wordPadding
return {short,long}
}
Insert cell
secondScaleX =
{
const closure = () => {
const scale = d3.scaleBand()
.domain(new Array(3).fill(0).map((d,i)=>i))
.range([Math.PI*4/3,Math.PI * 5/3]) // 120deg
const f = (index) =>scale(index % 3)
Object.setPrototypeOf(f,scale)
return f
}
return closure()
}
Insert cell
wordArc2 = d3.arc()
.innerRadius((d,i) => ScaleY(i).short)
.outerRadius((d,i) => ScaleY(i).long)
.startAngle((d,i) => secondScaleX(i))
.endAngle((d,i) => secondScaleX(i) + secondScaleX.bandwidth())
.padAngle(0.01)
.padRadius(outerRadius)
Insert cell
wordArc1 = d3.arc()
.innerRadius((d,i) => firstScaleY(i).short)
.outerRadius((d,i) => firstScaleY(i).long)
.startAngle((d,i) => firstScaleX(i))
.endAngle((d,i) => firstScaleX(i) + firstScaleX.bandwidth())
.padAngle(0.01)
.padRadius(outerRadius)
Insert cell
sliceLen = (wordRadius - outerRadius)/4
Insert cell
md`发言频率`
Insert cell
timeRange = 60
Insert cell
xMax = 20
Insert cell
test = [0,20,40,60]
Insert cell
speakTimes = {
const sentenceFromSpecificRole = data.filter(v => v.role === role)//筛选角色为B的发言
const len = Math.ceil(Number(data[data.length-1].endTime)/timeRange) //有多少个时间段
const speakTimes = new Array(len).fill(0) //各段时间发言次数
for(let i = 0 ; i < sentenceFromSpecificRole.length ; i ++)
{
let line = sentenceFromSpecificRole[i]
let index = Math.floor(Number(line.startTime)/timeRange)
speakTimes[index]++
}
return speakTimes
}
Insert cell
scaleBottomLineX = d3.scaleLinear()
.domain([0,speakTimes.length * timeRange])
.range([Math.PI * 4/3,Math.PI * 2/3])
Insert cell
scaleBottomLineY = d3.scaleLinear()
.domain([0,xMax])
.range([wordRadius,outerRadius])
Insert cell
area = d3.areaRadial()
.curve(d3.curveCatmullRom)
.angle((d,i) => scaleBottomLineX(i * timeRange + timeRange/2))
Insert cell
smoothLine = d3.line()
.curve(d3.curveCatmullRom)
.x((d,i) => {
let angle = scaleBottomLineX(i * timeRange + timeRange/2)
let r = scaleBottomLineY(d)
return r * Math.sin(angle)
})
.y((d,i) => {
let angle = scaleBottomLineX(i * timeRange + timeRange/2)
let r = scaleBottomLineY(d)
return -r * Math.cos(angle)
})
Insert cell
scaleBottomLineY.ticks(5).reverse()
Insert cell
d = {
let angle = Math.PI * 4/3
let r = scaleBottomLineY(10)
let x1 = r * Math.sin(angle)
let y1 = -r * Math.cos(angle)
let x2 = r * Math.sin(Math.PI * 2/3)
let y2 = -r * Math.cos(Math.PI * 2/3)
return `M${x1},${y1} a${r},${r} 0 0,0 ${x2},${y2}`
}
Insert cell
yAxisBottom = g => g
.attr("text-anchor", "middle")
.attr("font-family", "sans-serif")
.attr("font-size", 10)
.call(g => g.selectAll("g")
.data(scaleBottomLineY.ticks(5).reverse())
.join("g")
.attr("fill", "none")
.call(g => g.append("path")
.attr("stroke", "#000")
.attr("stroke-opacity", 0.2)
.attr("d", d=>{
let angle1 = Math.PI * 4/3
let angle2 = Math.PI * 2/3
let r = scaleBottomLineY(d)
let x1 = r * Math.sin(angle1)
let y1 = -r * Math.cos(angle1)
let x2 = r * Math.sin(angle2)
let y2 = -r * Math.cos(angle2)
return `M${x1},${y1} A${r},${r} 0 0,0 ${x2},${y2} `
}))
.call(g=>g.append("text")
.attr("y",d=> scaleBottomLineY(d))
.attr("stroke","#fff")
.attr("stroke-width",4)
.text(d=>d.toFixed(0))
.clone(true)
.attr("fill","currentColor")
.attr("stroke","none")
)
)
Insert cell
role = "B"
Insert cell
chart = {
const svg = d3.select(DOM.svg(width, height))
.attr("viewBox", `${-width / 2} ${-height/2} ${width} ${height}`)
.style("width", width)
.style("height", height)
.style("font", "10px sans-serif");

const g = svg.append("g")
.attr("class","dts")
g.selectAll("g")
.data(dts)
.join("g")
.call(g=>{
g.append("path")
.attr("fill", (d,i)=>color(i))
.attr("d", arc)
g.append("text")
.attr('text-anchor', 'left')
.attr("fill-opacity","0.8")
.attr("font-size","0.7em")
.attr("transform",d => {
const angle = x(d.name) + x.bandwidth()/2
const radius = (y(d.val) + y(0))/2
const xx = radius * Math.sin(angle)
const yy = -radius * Math.cos(angle)
return `translate(${xx},${yy}) rotate(${180/Math.PI * angle - 90})`
})
.text(d => d.name)
})
//cateogory-one
svg.append("g")
.attr("class","category-one")
.selectAll("g")
.data(words)
.join("g")
.call(g => {
g.append("path")
.attr("fill", (d,i)=>color(i))
.attr("d", wordArc1)
g.append("clipPath") //限定绘制范围为数据对应的矩形大小,超出矩形的的文字不绘制
.attr("id",(d,i)=>("first_"+ i +"_clip"))//设置id并在数据结构中新建clipUid属性方便下面引用
.append("path")//矩形大小同上
.attr("d", wordArc1)
g.append("g")
.attr("clip-path",(d,i)=>`url(#first_${i}_clip)`)//引用clipPath来限制绘制范围
.append("text")
.attr('text-anchor', 'middle')
.attr("transform",(d,i)=>{
const angle = firstScaleX(i)+firstScaleX.bandwidth()/2
const {short,long} = firstScaleY(i)
const radius = (short+long)/2
const _x = radius * Math.sin(angle)
const _y = -radius * Math.cos(angle)
return `translate(${_x},${_y}) rotate(${180/Math.PI * angle })`
})
.text(d=>d)
})
svg.append("g")
.attr("class","category-two")
.selectAll("g")
.data(Task)
.join("g")
.call(g => {
g.append("path")
.attr("fill", "#6699ff")
.attr("fill-opacity",d=>d[1])
.attr("d", wordArc2)
g.append("clipPath") //限定绘制范围为数据对应的矩形大小,超出矩形的的文字不绘制
.attr("id",(d,i)=>("second_"+ i +"_clip"))//设置id并在数据结构中新建clipUid属性方便下面引用
.append("path")//矩形大小同上
.attr("d", wordArc2)
g.append("g")
.attr("clip-path",(d,i)=>`url(#second_${i}_clip)`)//引用clipPath来限制绘制范围
.append("text")
.attr("font-size","1em")
.attr('text-anchor', 'middle')
.attr("transform",(d,i)=>{
const angle = secondScaleX(i)+secondScaleX.bandwidth()/2
const {short,long} = ScaleY(i)
const radius = (short+long)/2
const _x = radius * Math.sin(angle)
const _y = -radius * Math.cos(angle)
return `translate(${_x},${_y}) rotate(${180/Math.PI * angle })`
})
.text(d=>d[0])
})
svg.append("g")
.append("circle")
.attr("r",outerRadius)
.attr("fill","none")
.attr("stroke","black")
.attr("stroke-dasharray","4")
svg.append("g")
.append("circle")
.attr("r",wordRadius)
.attr("fill","none")
.attr("stroke","black")
.attr("stroke-dasharray","4")
svg.append("g")
.call(g => {
const arc = d3.arc()
g.append("path")
.attr("id","wordRadius")
.attr("d",arc({
innerRadius: wordRadius+10,
outerRadius: wordRadius+10,
startAngle: firstScaleX(0),
endAngle: firstScaleX(5)
}))
.attr("fill","none")
g.append("text")
.attr("fill", "black")
.attr('text-anchor', 'middle')
.attr("font-size","2em")
.attr("x",425)
.append("textPath")
.attr("href","#wordRadius")
.text("category-one")
})
//折线图
const LineGraph = svg.append("g")
.attr("class","lineGraph")
LineGraph.append("path")
.attr("fill","none")
.attr('stroke', '#ff0000')
.attr('stroke-width', 1)
.attr('d', smoothLine(speakTimes) )
LineGraph.append("path")
.attr("fill", "steelblue")
.attr("fill-opacity", 0.2)
.attr("d", area
.outerRadius(scaleBottomLineY(0))
.innerRadius(d => scaleBottomLineY(d))
(speakTimes))

svg.append("g")
.call(yAxisBottom)
return svg.node();
}
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