import * as d3 from 'd3'
import { iconDrawObj, getG, getData, drawTopMask, drawBottomMask, initArrow, lineArrow, drawSpecialText, drawTagText, drawBottomSpecialText, drawSpecialNoTimeText,
drawSurgery,
} from './drawfn'
import { getMaxList } from './dataProcess'
import {
timeNumber,
nightTime,
leftTEXT,
showPain,
painTEXT,
bodyTemperature,
painScore,
starNumEnv,
endNumEnv,
heartRange,
INFO_KEYS,
Header,
TOP_KEYS,
BOTTOM_KEYS,
HEAD_HEIGHT,
LINE_HEIGHT,
textLeftMargin,
TEXT_MARGIN_BOTTOM
} from './config'
import ViewConfig from './ViewConfig'
export async function init(data = []) {
const ajaxData = getData(data)
const chart = ConnectedScatterplot({
x: (d) => d.year,
y: (d) => d.gas,
xType: d3.scaleBand,
// yDomain: [starNumEnv1, starNumEnv2, endNumEnv1, endNumEnv2],
yDomain: [starNumEnv, endNumEnv],
width: 1080,
height: 1290 + BOTTOM_KEYS.length * 20 + 7,
marginLeft: 10,
marginRight: 100,
marginTop: 10,
marginBottom: 80,
duration: 5000, // for the intro animation; 0 to disable
renderData: ajaxData
})
await nextTick() // 等待DOM更新
if (document.getElementById('my_dataviz').children.length > 0) {
document.getElementById('my_dataviz').removeChild(document.getElementById('my_dataviz').children[0])
}
document.getElementById('my_dataviz').appendChild(chart)
}
function ConnectedScatterplot(options) {
// 计算出各种位置常量
const viewConfig = new ViewConfig(options)
let tooltip
const svg = d3
.create('svg')
.attr('id', 'printsvg')
.attr('width', viewConfig.width)
.attr('height', viewConfig.height)
.attr('viewBox', [0, 0, viewConfig.width, viewConfig.height])
.attr('style', 'height: auto; height: intrinsic;')
.attr('transform', `translate(0,${viewConfig.marginTop})`)
setTimeout(() => {
tooltip = svg.append('g').style('pointer-events', 'none')
})
const xScale = d3
.scaleBand()
.domain(d3.range(42))
.rangeRound(viewConfig.xRange)
// 体温的 scale
const bodyScale = d3.scaleLinear(
[bodyTemperature[0], bodyTemperature[1]],
viewConfig.yRange
)
// const y2Scale = d3.scaleLinear()
// .domain([94, 107]) // 新的 y 轴范围
// .range([viewConfig.height, 0]) // SVG 高度,0 表示顶端
// 心率的 Scale
const heartScale = d3.scaleLinear(
[heartRange[0], heartRange[1]], // [20, 180];
viewConfig.yRange // [560,60];
)
// 心率上限
const bodyOverflowData = getMaxList({
list: viewConfig.renderData.datasetHeartRate.flat(Infinity),
max: heartRange[1],
min: heartRange[0],
maxDefault: 180,
minDefault: 40
})
// 脉搏上限
const datasetPulse = getMaxList({
list: viewConfig.renderData.datasetPulse.flat(Infinity),
max: heartRange[1],
min: heartRange[0],
maxDefault: 180,
minDefault: 40
})
//口温下限
const bodyData = getMaxList({
list: viewConfig.renderData.bodyData,
max: bodyTemperature[1],
min: bodyTemperature[0],
maxDefault: 42,
minDefault: 35
});
// 腋温下限
const datasetAnus = getMaxList({
list: viewConfig.renderData.datasetAnus,
max: bodyTemperature[1],
min: bodyTemperature[0],
maxDefault: 42,
minDefault: 35
})
// 肛温下限
const datasetHeartrate = getMaxList({
list: viewConfig.renderData.datasetHeartrate,
max: bodyTemperature[1],
min: bodyTemperature[0],
maxDefault: 42,
minDefault: 35
})
// 耳温下限
const earCool = getMaxList({
list: viewConfig.renderData.earCool,
max: bodyTemperature[1],
min: bodyTemperature[0],
maxDefault: 42,
minDefault: 35
})
const allTemperature = [bodyOverflowData, datasetPulse, bodyData, datasetAnus, datasetHeartrate, earCool]
// viewConfig.renderData.bodyData = levelingData({
// list: viewConfig.renderData.bodyData,
// maxDefault: 43,
// minDefault: 33
// })
initArrow(svg)
// ==========================
// 开始调用绘制函数
// ==========================
// 1、绘制竖线格子
// 2、绘制折线图
// 3、绘制上下的字段属性(遮挡住超出的线条)【需要控制一个上下限,不超过遮罩层,体温【43°,】】
// 4、绘制被遮挡的竖线格子
// 一定要按照循序调用
// 绘制脉搏体温
drwaPulse(svg)
// 绘制折线区域格子
drawbgLine(svg)
// 绘制红色竖线
drwaVerticallLine(svg)
// 绘制折线
viewConfig.renderData.brokenLineData.forEach(item => {
brokenLine(svg, item)
})
const tmpList = getIndexValue(
viewConfig.renderData.bodyData,
viewConfig.renderData.datasetAnus,
viewConfig.renderData.datasetHeartrate,
viewConfig.renderData.earCool
)
// 绘制脉搏线条
viewConfig.renderData.datasetPulse.forEach((item) => {
const item2 = []
item.forEach((x) => {
// const obj = tmpList.find((o) => o.index === x.index && o.value === heartScale(x.value))
const obj = tmpList.find((o) => o.index === x.index && Math.abs(o.value - heartScale(x.value)) < 1)
if (!obj) {
item2.push(x)
}
})
drawHeart(svg, item, true)
drawHeart(svg, item2, false)
})
// 绘制心率线条
viewConfig.renderData.datasetHeartRate.forEach((item) => {
drawHeartRate(svg, item)
})
// // 绘制口温线条
// drawPathBody(svg, viewConfig.renderData.bodyData)
// 绘制腋温线条
drawAnus(svg, viewConfig.renderData.bodyData)
// // 绘制肛温线条
// drawJuhua(svg, viewConfig.renderData.datasetHeartrate)
// // 绘制耳温
// thermometer(svg, viewConfig.renderData.earCool)
// 绘制疼痛
// if (showPain) {
// viewConfig.renderData.painScore.forEach(item => {
// drawPain(svg, item);
// });
// }
// 需要放在绘制折线后,起到遮挡折线的作用
// 绘制呼吸格子
drwaBreathing(svg, viewConfig.renderData.datasetPain.filter(x => x.value !== null))
// 需要放在绘制呼吸后,绘制下划线
// 绘制底部横线和值
// ===================== //
// 遮罩层挡住超出的折线 //
// =====================//
drawTopMask(svg, viewConfig)
drawBottomMask(svg, viewConfig)
// 绘制底部线条
drwaBottomLine(svg)
drwaBottomLineData(svg)
// 绘制顶部值
drwaTopData(svg)
// 绘制特殊事件文字---有时间
if (viewConfig.renderData.otherArr.length > 0) {
for (let j = 0; j < viewConfig.renderData.otherArr.length; j++) {
drawSpecialText(svg, viewConfig, viewConfig.renderData.otherArr[j])
}
}
drawSurgery(svg, viewConfig, viewConfig.renderData.minSurgeryArr)
drawSurgery(svg, viewConfig, viewConfig.renderData.surgeryArr)
drawSpecialNoTimeText(svg, viewConfig, viewConfig.renderData.temArr)
//drawSpecialNoTimeText(svg, viewConfig, viewConfig.renderData.smallSurgerytArr)
//drawSpecialNoTimeText(svg, viewConfig, viewConfig.renderData.dischargeArr)
// drawSpecialNoTimeText(svg, viewConfig, viewConfig.renderData.transferArr)
// 绘制标签文字
// drawTagText(svg, viewConfig, viewConfig.renderData.symbolContent)
// drawBottomSpecialText(svg, viewConfig, viewConfig.renderData.symbolDegreesOnline);
drawBottomSpecialText(svg, viewConfig, viewConfig.renderData.symbolGoUp)
drawBottomSpecialText(svg, viewConfig, viewConfig.renderData.symbolDegreesEvents)
// 绘制降温的红圈和虚线
drawCoolBody(svg, viewConfig.renderData.dataCool, viewConfig.renderData.allTemperatureData)
// 绘制同一时间心率和脉搏的阴影
drawRateBody(svg, viewConfig.renderData.datasetPulse, viewConfig.renderData.datasetHeartRate)
drwaOtherInfo(svg, viewConfig.renderData.infoData)
// 最后一步
// 绘制四周加粗边框
drwaTabelBorder(svg)
// ========== //
// 最后展示浮层的内容
// ========== //
allTemperature.forEach((item) => {
lineArrow({
svg,
viewConfig,
scale: xScale,
data: item
})
})
// ==========================
// 结束调用绘制函数
// ==========================
return svg.node()
// 绘制最外面边框
function drwaTabelBorder(svg) {
const g = svg
.append('g')
.attr(
'transform',
`translate(${viewConfig.marginLeft},${viewConfig.topPos})`
)
g.append('rect')
.attr('x', 0)
.attr('y', 0)
.attr('width', viewConfig.contentWidth)
.attr('height', viewConfig.tableHeight)
.attr('stroke', viewConfig.stroke)
.attr('fill', 'none')
.attr('style', 'stroke-width: 4')
}
// 绘制所有竖线
function drwaVerticallLine(svg) {
const g = getG(svg, viewConfig)
let start = viewConfig.step
g.attr('class', 'maskline2')
while (start < viewConfig.contentWidth) {
// const isLastLine = (start + viewConfig.step) >= viewConfig.contentWidth
g.append('line')
.attr('x1', start)
.attr('y1', 0)
.attr('y2', viewConfig.tableHeight)
.attr('x2', start)
.attr('stroke', start > viewConfig.step ? '#B22222' : viewConfig.stroke)
// .attr('stroke', isLastLine ? 'black' : (start > viewConfig.step ? '#B22222' : viewConfig.stroke))
.attr('stroke-width', 4) // 设置线条宽度为2
start = start + viewConfig.step
}
}
// 绘制底部的线条
function drwaBottomLine(svg) {
// 补上下竖线
let start = viewConfig.step
const lineG = getG(svg, viewConfig)
.append('g')
.attr('class', 'maskline')
const topPos = viewConfig.bottomKeysPosStart + 40
while (start < viewConfig.contentWidth) {
BOTTOM_KEYS.forEach((item, i) => {
const step = viewConfig.step / item.times
let itemStart = start
for (let j = 0; j < 6; j++) {
if (((j * viewConfig.step) / 6) % step === 0) {
lineG
.append('line')
.attr('fill', 'stroke')
.attr('x1', itemStart)
.attr('y1', topPos + i * LINE_HEIGHT)
.attr('y2', topPos + (i + 1) * LINE_HEIGHT)
.attr('x2', itemStart)
.attr('stroke', viewConfig.stroke)
}
itemStart += viewConfig.step / 6
}
})
start += viewConfig.step
}
}
// 绘制底部的线条和文字
function drwaBottomLineData(svg) {
const g = getG(svg, viewConfig)
// 绘制底部的横向线条
g.selectAll('line')
.data([...BOTTOM_KEYS])
.join('line')
.attr('x1', 0)
.attr('y1', (d, i) => {
const isLast = i === BOTTOM_KEYS.length - 1;
return viewConfig.bottomKeysPosStart + (i + 2) * LINE_HEIGHT + (isLast ? 15 : 0);
// return viewConfig.bottomKeysPosStart + (i + 2) * LINE_HEIGHT // 2是呼吸,占了2行
})
.attr('y2', (d, i) => {
const isLast = i === BOTTOM_KEYS.length - 1;
return viewConfig.bottomKeysPosStart + (i + 2) * LINE_HEIGHT + (isLast ? 15 : 0)
// return viewConfig.bottomKeysPosStart + (i + 2) * LINE_HEIGHT
})
.attr(
'x2',
viewConfig.width - viewConfig.marginLeft - viewConfig.marginRight
)
.attr('fill', 'none')
.attr('class', 'dataLine')
.attr('stroke', viewConfig.stroke)
.attr('stroke-width', 1)
.attr('stroke-linejoin', viewConfig.strokeLinejoin)
.attr('stroke-linecap', viewConfig.strokeLinecap)
const textArr = BOTTOM_KEYS
const repeatArr = d3.range(8)
// 绘制文字
textArr.map(({ key, name, times }, index) => {
if (key !== '100') {
if (times && times === 2) {
let str = []
g.append('g')
.selectAll('text')
.data(repeatArr)
.join('text')
.attr('style', 'font-size:15px; font-weight:bold;fill: black')
.attr('class', 'mytext')
.text((i) => {
if (i == 0) {
return name
} else {
const text = getTypeValue(key, viewConfig.renderData.typesData)[i - 1]?.typeValue
if (text) {
str = text.split(',')
if (str.length > 1) {
return str[0]
}
}
return text
}
})
.attr('x', (i) => {
const pos = getTypeValue(key, viewConfig.renderData.typesData)[i - 1]?.index + 1
if (i === 0) {
return textLeftMargin * 2
}
return pos * viewConfig.step + textLeftMargin
})
.attr('y', () => {
return viewConfig.bottomKeysPosStart + (index + 3) * LINE_HEIGHT - TEXT_MARGIN_BOTTOM
})
g.append('g')
.selectAll('text')
.data(repeatArr)
.join('text')
.attr('style', 'font-size:15px; font-weight:bold;fill: black')
.attr('class', 'mytext')
.text((i) => {
if (i == 0) {
return ''
} else {
const text = getTypeValue(key, viewConfig.renderData.typesData)[i - 1]?.typeValue
if (text) {
str = text.split(',')
if (str.length > 1) {
return str[1]
}
}
return ''
}
})
.attr('x', (i) => {
const pos = getTypeValue(key, viewConfig.renderData.typesData)[i - 1]?.index + 1
if (i === 0) {
// return textLeftMargin * 2
}
return pos * viewConfig.step + textLeftMargin + 60
})
.attr('y', () => {
return viewConfig.bottomKeysPosStart + (index + 3) * LINE_HEIGHT - TEXT_MARGIN_BOTTOM
})
}
else if (key === '017') {
for (let j = 0; j < 8; j++) {
const testText = getTypeValue(key, viewConfig.renderData.typesData)[j - 1]?.typeValue
if (j === 0){
g.append('g')
.selectAll('text')
.data(repeatArr)
.join('text')
.attr('style', 'font-size:15px;')
.attr('class', 'mytext')
.text(name)
.attr('x', () => {
return textLeftMargin * 2 + 10
})
.attr('y', () => {
return viewConfig.bottomKeysPosStart + (index + 3) * LINE_HEIGHT - TEXT_MARGIN_BOTTOM + 1
})
}
else if (testText !== undefined){
if(testText.length > 8){
for (let i = 0; i < testText.length; i += 8) {
// 使用slice方法获取当前段的字符串
const line = testText.slice(i, i + 8);
g.append('g')
.selectAll('text')
.data(repeatArr)
.join('text')
.attr('style', 'font-size:14px')
.attr('class', 'mytext')
.text(line)
.attr('x', () => {
const pos = getTypeValue(key, viewConfig.renderData.typesData)[j - 1]?.index + 1
return pos * viewConfig.step + textLeftMargin
})
.attr('y', () => {
return viewConfig.bottomKeysPosStart + (index + 3) * LINE_HEIGHT - TEXT_MARGIN_BOTTOM + i*2.5
})
}
}else{
g.append('g')
.selectAll('text')
.data(repeatArr)
.join('text')
.attr('style', 'font-size:14px;')
.attr('class', 'mytext')
.text(testText)
.attr('x', () => {
const pos = getTypeValue(key, viewConfig.renderData.typesData)[j - 1]?.index + 1
return pos * viewConfig.step + textLeftMargin
})
.attr('y', () => {
return viewConfig.bottomKeysPosStart + (index + 3) * LINE_HEIGHT - TEXT_MARGIN_BOTTOM
})
}
}
}
}
else {
g.append('g')
.selectAll('text')
.data(repeatArr)
.join('text')
// .attr('style', 'font-size:15px; font-weight:bold')
.attr('style', (i, d) => { // 使用回调函数来设置样式
let textContent = (i === 0) ? name : getTypeValue(key, viewConfig.renderData.typesData)[i - 1]?.typeValue;
let fontSize = '15px'; // 默认字体大小margin-left: 15px
// 检查条件并设置不同的字体大小
if (key === '005' && textContent === '*') {
fontSize = '30px'; // 或者你想要的任何特定大小
}
// 构建并返回样式字符串
return `font-size: ${fontSize};font-weight:bold;`; // 注意这里只设置了字体大小,其他样式未受影响
})
.attr('class', 'mytext')
.text((i) => {
if (i === 0) {
return name
} else {
let textValue = getTypeValue(key, viewConfig.renderData.typesData)[i - 1]?.typeValue
return textValue
}
})
.attr('x', (i) => {
let nameLocation = textLeftMargin * 2 + 10
if (i === 0) {
if(key === '006'){
nameLocation = textLeftMargin * 2 - 3
}
return nameLocation
}
const pos = getTypeValue(key, viewConfig.renderData.typesData)[i - 1]?.index + 1
const typesData = getTypeValue(key, viewConfig.renderData.typesData)[i-1]
if (typesData) {
const firstNameLength = typesData.typeValue.length
if (firstNameLength === 1) {
return pos * viewConfig.step + textLeftMargin + 50
} else if (firstNameLength === 2) {
return pos * viewConfig.step + textLeftMargin + 46
} else if (firstNameLength === 3) {
return pos * viewConfig.step + textLeftMargin + 43
} else if (firstNameLength === 4) {
if (typesData.typeCode === '010') {
return pos * viewConfig.step + textLeftMargin + 45
} else {
return pos * viewConfig.step + textLeftMargin + 40
}
} else if (firstNameLength === 5) {
return pos * viewConfig.step + textLeftMargin + 38
} else if (firstNameLength === 6) {
return pos * viewConfig.step + textLeftMargin + 32
} else if (firstNameLength > 10) {
return pos * viewConfig.step + textLeftMargin
} else {
return pos * viewConfig.step + textLeftMargin + 32
}
} else {
return pos * viewConfig.step + textLeftMargin + 30
}
})
.attr('y', (i) => {
const daBianValue = getTypeValue(key, viewConfig.renderData.typesData)[i - 1]?.typeValue
let textHeight = viewConfig.bottomKeysPosStart + (index + 3) * LINE_HEIGHT - TEXT_MARGIN_BOTTOM
if(key === '005' && daBianValue === '*'){
textHeight = textHeight + 12
}
return textHeight
})
// .attr('y', () => {
// return (
// viewConfig.bottomKeysPosStart + (index + 3) * LINE_HEIGHT - TEXT_MARGIN_BOTTOM + 1
// )
// })
}
}
})
}
// 绘制底部的线条和文字
// function drwaBottomLineData(svg) {
// const g = getG(svg, viewConfig)
// // 绘制底部的横向线条
// g.selectAll('line')
// .data([...BOTTOM_KEYS])
// .join('line')
// .attr('x1', 0)
// .attr('y1', (d, i) => {
// return viewConfig.bottomKeysPosStart + (i + 2) * LINE_HEIGHT // 2是呼吸,占了2行
// })
// .attr('y2', (d, i) => {
// return viewConfig.bottomKeysPosStart + (i + 2) * LINE_HEIGHT
// })
// .attr('x2', viewConfig.width - viewConfig.marginLeft - viewConfig.marginRight)
// .attr('fill', 'none')
// .attr('class', 'dataLine')
// .attr('stroke', viewConfig.stroke)
// .attr('stroke-width', 1)
// .attr('stroke-linejoin', viewConfig.strokeLinejoin)
// .attr('stroke-linecap', viewConfig.strokeLinecap)
// const textArr = BOTTOM_KEYS
// const repeatArr = d3.range(8)
// // 绘制文字
// textArr.map(({ key, name, times }, index) => {
// // 绘制血压
// if (times && times === 2) {
// let str = []
// g.append('g')
// .selectAll('text')
// .data(repeatArr)
// .join('text')
// .attr('style', 'font-size:14px')
// .attr('class', 'mytext')
// .text((i) => {
// if (i === 0) {
// return name
// } else {
// const text = getTypeValue(key, viewConfig.renderData.typesData)[i - 1]?.typeValue
// if (text) {
// str = text.split(',')
// if (str.length > 1) {
// return str[0]
// }
// }
// return text
// }
// })
// .attr('x', (i) => {
// const pos = getTypeValue(key, viewConfig.renderData.typesData)[i - 1]?.index + 1
// if (i === 0) {
// return textLeftMargin * 2
// }
// return pos * viewConfig.step + textLeftMargin
// })
// .attr('y', () => {
// return viewConfig.bottomKeysPosStart + (index + 3) * LINE_HEIGHT - TEXT_MARGIN_BOTTOM
// })
// g.append('g')
// .selectAll('text')
// .data(repeatArr)
// .join('text')
// .attr('style', 'font-size:14px; margin-left: 5px')
// .attr('class', 'mytext')
// .text((i) => {
// if (i === 0) {
// return name
// } else {
// const text = getTypeValue(key, viewConfig.renderData.typesData)[i - 1]?.typeValue
// if (text) {
// str = text.split(',')
// if (str.length > 1) {
// return str[1]
// }
// }
// return ''
// }
// })
// .attr('x', (i) => {
// const pos = getTypeValue(key, viewConfig.renderData.typesData)[i - 1]?.index + 1
// if (i === 0) {
// return textLeftMargin * 2
// }
// return pos * viewConfig.step + textLeftMargin + 60
// })
// .attr('y', () => {
// return viewConfig.bottomKeysPosStart + (index + 3) * LINE_HEIGHT - TEXT_MARGIN_BOTTOM
// })
// } else {
// g.append('g')
// .selectAll('text')
// .data(repeatArr)
// .join('text')
// // .attr('style', 'font-size:14px')
// .attr('style', (i, d) => { // 使用回调函数来设置样式
// let textContent = (i === 0) ? name : getTypeValue(key, viewConfig.renderData.typesData)[i - 1]?.typeValue;
// let fontSize = '14px'; // 默认字体大小margin-left: 5px
// // 检查条件并设置不同的字体大小
// if (key === '005' && textContent === '*') {
// fontSize = '30px'; // 或者你想要的任何特定大小
// }
// // 构建并返回样式字符串
// return `font-size: ${fontSize};`; // 注意这里只设置了字体大小,其他样式未受影响
// })
// .attr('class', 'mytext')
// .text((i) => {
// if (i === 0) {
// return name
// } else {
// return getTypeValue(key, viewConfig.renderData.typesData)[i - 1]?.typeValue
// }
// })
// .attr('x', (i) => {
// const pos = getTypeValue(key, viewConfig.renderData.typesData)[i - 1]?.index + 1
// if (i === 0) {
// return textLeftMargin * 2
// }
// return pos * viewConfig.step + textLeftMargin
// })
// .attr('y', (i) => {
// const daBianValue = getTypeValue(key, viewConfig.renderData.typesData)[i - 1]?.typeValue
// let textHeight = viewConfig.bottomKeysPosStart + (index + 3) * LINE_HEIGHT - TEXT_MARGIN_BOTTOM
// if(key === '005' && daBianValue === '*'){
// textHeight = textHeight + 12
// }
// return textHeight
// })
// }
// })
// }
// 绘制呼吸
function drwaBreathing(svg, breathData) {
const g = getG(svg, viewConfig)
// 遮罩层挡住超出的折线
g.append('rect')
.attr('class', 'mask-rect')
.attr('x', 0)
.attr('y', viewConfig.bottomKeysPosStart)
.attr(
'width',
viewConfig.width - viewConfig.marginRight - viewConfig.marginLeft
)
.attr('height', viewConfig.micoStep * 2 - 1)
.attr('stroke', viewConfig.stroke)
.attr('fill', '#fff')
.attr('style', 'stroke-width: 0')
// 绘制横线
g.append('line')
.attr('x1', 0)
.attr('y1', viewConfig.bottomKeysPosStart)
.attr('y2', viewConfig.bottomKeysPosStart)
.attr(
'x2',
viewConfig.width - viewConfig.marginLeft - viewConfig.marginRight
)
.attr('fill', 'none')
.attr('class', 'dataLine')
.attr('stroke', viewConfig.stroke)
.attr('stroke-width', 1)
.attr('stroke-linejoin', viewConfig.strokeLinejoin)
.attr('stroke-linecap', viewConfig.strokeLinecap)
const textYPos =
viewConfig.bottomKeysPosStart + LINE_HEIGHT + TEXT_MARGIN_BOTTOM
// 绘制汉字
g.append('text')
.attr('style', 'font-size:15px;font-weight:bold')
.text('呼 吸(次/分)')
.attr('x', textLeftMargin * 2)
.attr('y', textYPos)
// 绘制数据
const data = d3.range(42)
g.append('g')
.selectAll('text')
.data(data)
.join('text')
.attr('style', 'font-size:14px')
.attr('class', 'mytext')
.text((d) => {
return breathData[d]?.value
})
.attr('x', (i) => {
const index = breathData[i]?.index
return viewConfig.step + index * viewConfig.micoStep + 3
})
.attr('y', (i) => {
if (i % 2) {
return textYPos + 10
} else {
return textYPos - 10
}
})
// 绘制呼吸竖线
g.append('g')
.selectAll('line')
.data(data)
.join('line')
.attr('x1', (d, i) => {
return viewConfig.step + i * viewConfig.micoStep
})
.attr('y1', viewConfig.bottomKeysPosStart + 2 * LINE_HEIGHT)
.attr('x2', (d, i) => {
return viewConfig.step + i * viewConfig.micoStep
})
.attr('y2', viewConfig.bottomKeysPosStart)
.attr('fill', 'none')
.attr('class', 'dataLine')
.attr('stroke', viewConfig.stroke)
.attr('stroke-width', 1)
.attr('stroke-linejoin', viewConfig.strokeLinejoin)
.attr('stroke-linecap', viewConfig.strokeLinecap)
}
// 绘制脉搏体温文字
function drwaPulse(svg) {
// 左侧的文字
const g = getG(svg, viewConfig)
g.append('line')
.attr('class', 'slefline')
.attr('y1', viewConfig.topKeysPos) // 这个还要修改一下
.attr('x1', viewConfig.step / 2)
.attr('y2', viewConfig.bottomKeysPosStart)
.attr('x2', viewConfig.step / 2)
.attr('stroke', viewConfig.stroke)
// 画刻度尺
// for (let i = 5; i <= 70; i++) {
// const yPosition = viewConfig.topKeysPos + viewConfig.micoStep * 2.8 * i / 5 + 50
// let lineLength = 9 // 设置加长的刻度线长度
// if (i % 5 === 0) {
// lineLength = 16
// }
// 画刻度线
// g.append('line')
// .attr('x1', viewConfig.step / 2) // 刻度线的x坐标
// .attr('y1', yPosition)
// .attr('x2', viewConfig.step / 2 - lineLength) // 刻度线的结束坐标
// .attr('y2', yPosition)
// .attr('stroke', viewConfig.stroke)
//.attr('stroke-width', 2)
//}
leftTEXT.map((texts, index) => {
g.append('g')
.selectAll('text')
.data(texts)
.join('text')
.attr('style', `font-size:14px;fill:${['red', 'blue'][index]}`)
.attr('class', 'mytext')
.html((d, i) => {
if (i === 0) {
const value = d.split(',')
return `${value[0]}${value[1]}${value[2]}`
}
return `${d}`
})
.attr('x', (d, i) => {
// 调整坐标文字显示位置
if (i === 0) {
if (index === 0) {
return viewConfig.micoStep * Math.max(index * 4, 1) - 12
}
return viewConfig.micoStep * Math.max(index * 4, 1) - 6
}
return viewConfig.micoStep * Math.max(index * 4, 0.9)
})
.attr('y', (d, i) => {
if (i === 0) {
return viewConfig.topKeysPos + viewConfig.micoStep * 1
}
return viewConfig.topKeysPos + viewConfig.micoStep * 5 * i
})
})
// 绘制疼痛表格
if (showPain) {
// 绘制疼痛分割线
g.append('line')
.attr('x1', 0)
.attr('y1', viewConfig.bottomKeysPosStart - viewConfig.micoStep * 6)
.attr('y2', viewConfig.bottomKeysPosStart - viewConfig.micoStep * 6)
.attr('x2', viewConfig.width - viewConfig.marginLeft - viewConfig.marginRight)
.attr('fill', 'none')
.attr('class', 'dataLine')
.attr('stroke', viewConfig.stroke)
.attr('stroke-width', 1)
.attr('stroke-linejoin', viewConfig.strokeLinejoin)
.attr('stroke-linecap', viewConfig.strokeLinecap)
painTEXT.map((texts, index) => {
g.append('g')
.selectAll('text')
.data(texts)
.join('text')
.attr('style', `font-size:14px;fill:${['black', 'blue'][index]}`)
.attr('class', 'mytext')
.html((d, i) => {
if (d.length > 2) {
const value = d.split(' ')
return `${value[0]}${value[1]}${value[2]}${value[3]}`
} else if (i === 0) {
return `${d}`
}
return `${d}`
})
.attr('x', (d, i) => {
// 调整坐标文字显示位置
if (i === 0) {
if (index === 0) {
return viewConfig.micoStep * Math.max(index * 4, 1) - 12
}
return viewConfig.micoStep * Math.max(index * 4, 1) - 6
}
return viewConfig.micoStep * Math.max(index * 4, 0.9)
})
.attr('y', (d, i) => {
if (i === 0) {
return viewConfig.bottomKeysPosStart - viewConfig.micoStep * 3.8 - 30
}
return viewConfig.bottomKeysPosStart - viewConfig.micoStep * 3.8 + viewConfig.micoStep * i - 30
})
})
}
}
// 绘制折线的背景格子
function drawbgLine(svg) {
const g = getG(svg, viewConfig)
// 绘制横线
const horizontallength = (bodyTemperature[1] - bodyTemperature[0] + 1) * 5
const horizontalData = [...new Array(horizontallength).keys()]
g.append('g')
.attr('class', 'line-content')
.selectAll('line')
.data(horizontalData)
.join('line')
// .attr('x1', viewConfig.step)
.attr('x1', (d, i) => {
// 判断是否是最后五条线,如果是,则减少几厘米
if (i >= horizontalData.length - 9) {
return viewConfig.step - 60 // 向前缩短20像素
}
// 判断是否是最后五条线,如果是,则减少几厘米
if (i >= horizontalData.length - 10) {
return viewConfig.step -120 // 向前缩短20像素
}
return viewConfig.step // 其他线使用默认长度
})
.attr('y1', (d, i) => {
return viewConfig.topKeysPos + i * viewConfig.micoStep
})
.attr('y2', (d, i) => {
return viewConfig.topKeysPos + i * viewConfig.micoStep
})
// .attr('x2', (d, i) => {
// // 判断是否是最后五条线,如果是,则减少几厘米
// if (i >= horizontalData.length - 10) {
// return viewConfig.width - viewConfig.marginLeft - viewConfig.marginRight // 向前缩短20像素
// }
// return viewConfig.width - viewConfig.marginLeft - viewConfig.marginRight // 其他线使用默认长度
// })
.attr('x2', viewConfig.width - viewConfig.marginLeft - viewConfig.marginRight)
.attr('fill', 'none')
.attr('class', 'dataLine')
.attr('stroke', viewConfig.stroke)
.attr('stroke-width', 1)
.attr('stroke-linejoin', viewConfig.strokeLinejoin)
.attr('stroke-linecap', viewConfig.strokeLinecap)
.attr('style', (d, i) => {
if (i === horizontalData.length - 5) {
return 'stroke-width: 1; stroke: black;' // 不绘制最后一条线
}
if (i % 5 === 0 && i !== 0) {
return 'stroke-width: 3; stroke: blue;'
}
return 'stroke-width: 1'
})
// 竖线绘制
const verticalData = [...new Array(42).keys()]
g.append('g')
.selectAll('line')
.data(verticalData)
.join('line')
.attr('x1', (d, i) => {
return viewConfig.step + i * viewConfig.micoStep
})
.attr('y1', viewConfig.topKeysPos)
.attr('x2', (d, i) => {
return viewConfig.step + i * viewConfig.micoStep
})
.attr('y2', viewConfig.bottomKeysPosStart)
.attr('fill', 'none')
.attr('class', 'dataLine')
.attr('stroke', viewConfig.stroke)
.attr('stroke-width', 1)
.attr('stroke-linejoin', viewConfig.strokeLinejoin)
.attr('stroke-linecap', viewConfig.strokeLinecap)
g.append('line')
.attr('class', 'myline')
.attr('x1', 0)
.attr('y1', viewConfig.topKeysPos)
.attr('x2', viewConfig.width - viewConfig.marginLeft - viewConfig.marginRight)
.attr('y2', viewConfig.topKeysPos)
.attr('stroke', viewConfig.stroke)
.attr('stroke-width', 1)
.attr('stroke-linejoin', viewConfig.strokeLinejoin)
.attr('stroke-linecap', viewConfig.strokeLinecap)
}
// 绘制顶部数据
function drwaTopData(svg) {
const g = getG(svg, viewConfig)
// 绘制横向线条
g.selectAll('line')
.data(TOP_KEYS)
.join('line')
.attr('x1', 0)
.attr('y1', (d, i) => {
return LINE_HEIGHT * (i + 1)
})
.attr('y2', (d, i) => {
return LINE_HEIGHT * (i + 1)
})
.attr('x2', viewConfig.contentWidth)
.attr('fill', 'none')
.attr('class', 'newline')
.attr('stroke', viewConfig.stroke)
.attr('stroke-width', 1)
.attr('stroke-linejoin', viewConfig.strokeLinejoin)
.attr('stroke-linecap', viewConfig.strokeLinecap)
// 数据的值
const repeatArr = [...new Array(8).keys()]
// 绘制文字
TOP_KEYS.map(({ getValue, name }, index) => {
g.append('g')
.selectAll('text')
.data(repeatArr)
.join('text')
.attr('style', 'font-size:14px;text-anchor:middle;')
.attr('class', 'mytext')
.text((i) => {
if (i === 0) {
return name
} else {
return getValue(i - 1, viewConfig.renderData)
}
})
.attr('x', (i) => i * viewConfig.step + viewConfig.step / 2)
.attr('y', () => {
return LINE_HEIGHT * (index + 1) - TEXT_MARGIN_BOTTOM
})
})
// 绘制时间字段
// 绘制汉字
g.append('text')
.attr('style', 'font-size:14px;text-anchor:middle;')
.text('时 间')
.attr('x', viewConfig.step / 2)
.attr('y', viewConfig.topKeysPos - TEXT_MARGIN_BOTTOM)
// 绘制竖线和时间汉字
const data = new Array(timeNumber.length * 7).fill('').map((d, i) => timeNumber[i % timeNumber.length])
g.append('g')
.selectAll('text')
.data(data)
.join('text')
.attr('style', 'font-size:14px')
.attr('class', 'mytext')
.attr('fill', (d) => {
if (nightTime.includes(d)) {
return 'red'
} else {
return viewConfig.stroke
}
})
.text((d) => {
return d
})
.attr('x', (d, i) => {
return viewConfig.step + i * viewConfig.micoStep + 3
})
.attr('y', (d, i) => {
return viewConfig.topKeysPos - TEXT_MARGIN_BOTTOM
})
// 线条
g.append('g')
.attr('class', 'textYPos')
.selectAll('line')
.data(data)
.join('line')
.attr('x1', (d, i) => {
return viewConfig.step + i * viewConfig.micoStep
})
.attr('y1', viewConfig.topKeysPos - LINE_HEIGHT)
.attr('x2', (d, i) => {
return viewConfig.step + i * viewConfig.micoStep
})
.attr('y2', viewConfig.topKeysPos)
.attr('fill', 'none')
.attr('class', 'dataLine')
.attr('stroke', viewConfig.stroke)
.attr('stroke-width', (d, i) => {
return i % 6 ? 1 : 0
})
.attr('stroke-linejoin', viewConfig.strokeLinejoin)
.attr('stroke-linecap', viewConfig.strokeLinecap)
}
// 绘制折线
function brokenLine(svg, pathData) {
const I = d3.map(pathData, (_, i) => i)
const g = getG(svg, viewConfig)
const line = d3
.line()
.defined((i) => pathData[i].value)
.x((i) => {
return xScale(pathData[i].index) + viewConfig.X_OFFSET
})
.y((i) => bodyScale(pathData[i].value))
g.attr('class', 'body-line')
getDrawPath({
content: g,
line: line(I.filter((i) => pathData[i].value))
})
}
// 获取体温数据
function getIndexValue(list1, list2, list3, list4) {
let list = []
list = list.concat(list1.filter((x) => x.value !== null))
list = list.concat(list2.filter((x) => x.value !== null))
list = list.concat(list3.filter((x) => x.value !== null))
list = list.concat(list4.filter((x) => x.value !== null))
list.sort((x, y) => x.index - y.index)
return list.map((x) => {
return {
index: x.index,
value: bodyScale(x.value)
}
})
}
// // 绘制口温
// function drawPathBody(svg, pathData) {
// const g = getG(svg, viewConfig);
// g.on(
// 'pointerenter pointermove',
// generatePointer({
// pathData,
// type: '口温',
// yScaleInstance: bodyScale
// })
// ).on('pointerleave', pointerleft);
// iconDrawObj.getDrawRoundIcon({
// content: g,
// data: d3.range(pathData.length),
// x: (i) => {
// return xScale(pathData[i].index) + viewConfig.X_OFFSET;
// },
// y: (i) => bodyScale(pathData[i].value <= 35 ? 0 : pathData[i].value),
// r: 3
// });
// }
// // 绘制肛温
// function drawJuhua(svg, pathData) {
// const g = getG(svg, viewConfig);
// g.on(
// 'pointerenter pointermove',
// generatePointer({
// pathData,
// type: '肛温',
// yScaleInstance: bodyScale
// })
// ).on('pointerleave', pointerleft);
// iconDrawObj.getDrawRoundDotIcon({
// content: g,
// data: d3.range(pathData.length),
// x: (i) => xScale(pathData[i].index) + viewConfig.X_OFFSET,
// y: (i) => bodyScale(pathData[i].value <= 35 ? 0 : pathData[i].value),
// fill: 'white',
// deepFill: 'blue',
// r: 3
// });
// }
// // 绘制 耳温
// function thermometer(svg, pathData) {
// const g = getG(svg, viewConfig);
// g.on(
// 'pointerenter pointermove',
// generatePointer({
// pathData,
// type: '耳温',
// yScaleInstance: bodyScale
// })
// ).on('pointerleave', pointerleft);
// iconDrawObj.drawThreeIcon({
// content: g,
// data: d3.range(pathData.length),
// x: (i) => {
// return xScale(pathData[i].index) + viewConfig.X_OFFSET;
// },
// y: (i) => {
// return bodyScale(pathData[i].value <= 35 ? 0 : pathData[i].value);
// },
// fill: 'white',
// riangle: 24
// });
// }
// 绘制 疼痛曲线
function drawPain(svg, pathData) {
const g = getG(svg, viewConfig)
const I = d3.map(pathData, (_, i) => i)
g.on(
'pointerenter pointermove',
generatePointer({
pathData: viewConfig.renderData.painScore.flat(Infinity),
type: '疼痛',
yScaleInstance: heartScale
})
).on('pointerleave', pointerleft)
const line = d3
.line()
.defined((i) => pathData[i].value || pathData[i].value === 0)
.x((i) => {
return xScale(pathData[i].index) + viewConfig.X_OFFSET
})
.y((i) => {
if (pathData[i].value) {
return painScale(pathData[i].value / 10) + 10
}
return painScale(0) + 10
})
getDrawPath({
content: g,
line: line(I.filter((i) => pathData[i].value || pathData[i].value === 0)),
stroke: 'black'
})
// iconDrawObj.getDrawRoundIcon({
// content: g,
// data: d3.range(pathData.length),
// x: (i) => {
// return xScale(pathData[i].index) + viewConfig.X_OFFSET
// },
// y: (i) => {
// if (pathData[i].value) {
// return painScale(pathData[i].value / 10) + 10
// }
// return painScale(0) + 10
// },
// // riangle: 24,
// fill: 'black',
// stroke: 'black'
// })
iconDrawObj.drawThreeIcon({
content: g,
data: d3.range(pathData.length),
x: (i) => {
return xScale(pathData[i].index) + viewConfig.X_OFFSET
},
y: (i) => {
if (pathData[i].value) {
return painScale(pathData[i].value / 10) + 10
}
return painScale(0) + 10
},
riangle: 24
})
}
// 绘制脉搏
function drawHeart(svg, pathData, isEmpty) {
const I = d3.map(pathData, (_, i) => i)
const g = getG(svg, viewConfig)
g.on(
'pointerenter pointermove',
generatePointer({
pathData: viewConfig.renderData.datasetPulse.flat(Infinity),
type: '脉搏',
yScaleInstance: heartScale
})
).on('pointerleave', pointerleft)
const line = d3
.line()
.defined((i) => pathData[i].value)
.x((i) => {
return xScale(pathData[i].index) + viewConfig.X_OFFSET
})
.y((i) => heartScale(pathData[i].value || 0))
if(isEmpty){
getDrawPath({
content: g,
line: line(I.filter((i) => pathData[i].value)),
stroke: 'red'
})
}
iconDrawObj.getDrawRoundIcon({
content: g,
data: d3.range(pathData.length),
x: (i) => {
return xScale(pathData[i].index) + viewConfig.X_OFFSET
},
y: (i) => {
return heartScale(pathData[i].value)
},
fill: isEmpty ? 'transparent' : 'red',
stroke: 'red'
})
}
// 绘制心率
function drawHeartRate(svg, pathData) {
const I = d3.map(pathData, (_, i) => i)
const g = getG(svg, viewConfig)
g.on(
'pointerenter pointermove',
generatePointer({
pathData: viewConfig.renderData.datasetHeartRate.flat(Infinity),
type: '心率',
yScaleInstance: heartScale
})
).on('pointerleave', pointerleft)
const line = d3
.line()
.defined((i) => pathData[i].value)
.x((i) => {
return xScale(pathData[i].index) + viewConfig.X_OFFSET
})
.y((i) => heartScale(pathData[i].value || 0))
getDrawPath({
content: g,
line: line(I.filter((i) => pathData[i].value)),
stroke: 'red'
})
iconDrawObj.getDrawRoundIcon({
content: g,
data: d3.range(pathData.length),
x: (i) => {
return xScale(pathData[i].index) + viewConfig.X_OFFSET
},
y: (i) => {
return heartScale(pathData[i].value)
},
fill: 'white',
stroke: 'red'
})
}
// 腋温
function drawAnus(svg, pathData) {
const g = getG(svg, viewConfig)
g.on(
'pointerenter pointermove',
generatePointer({
pathData,
type: '体温',
yScaleInstance: bodyScale
})
).on('pointerleave', pointerleft)
iconDrawObj.getDrawXIcon({
content: g,
data: d3.range(pathData.length),
x: (i) => {
return xScale(pathData[i].index) + viewConfig.X_OFFSET
},
y: (i) => bodyScale(pathData[i].value ? (pathData[i].value <= 35 ? 0 : pathData[i].value) : 0)
// style: {
// stroke: 'black', // 线的颜色
// 'stroke-width': 6, // 加粗,设置合适的宽度
// fill: 'none' // 如果需要可以调整填充
// }
})
const line = d3
.line()
.x((d, i) => xScale(d.index) + viewConfig.X_OFFSET)
.y((d) => bodyScale(d.value ? (d.value <= 35 ? 0 : d.value) : 0))
.defined((d) => d.value !== null && d.value !== undefined) // 过滤无效数据点
g.append('path')
.attr('class', 'axillary-line')
.attr('d', line(pathData))
.attr('fill', 'none')
.attr('stroke', 'blue')
.attr('stroke-width', 2)
.on('pointerleave', () => tooltip.style('display', 'none'))
}
// 绘制一个线条
function getDrawPath({ content, line, stroke = 'blue' }) {
content
.append('path')
.attr('class', 'mylines')
.attr('fill', 'none')
.attr('stroke', stroke)
.attr('stroke-width', viewConfig.strokeWidth)
.attr('stroke-linejoin', viewConfig.strokeLinejoin)
.attr('stroke-linecap', viewConfig.strokeLinecap)
.attr('d', line)
}
// // 绘制降温的红圈和虚线
// function drawCoolBody(svg, coolData, allData) {
// console.log('绘制降温的红圈和虚线', coolData, allData)
// const g = getG(svg, viewConfig)
// const lineData = [];
// // 遍历data1中的每个对象
// coolData.forEach(item1 => {
// // 如果item1有index和date属性
// if (item1.index !== undefined && item1.date !== undefined) {
// // 遍历data2中的每个对象
// allData.forEach(item2 => {
// // 如果item2也有index和date属性,并且它们与item1的相等
// if (item2.index === item1.index && item2.date === item1.date) {
// // 将匹配的对象添加到结果数组中
// // 这里可以添加整个对象,或者根据需要添加对象的某些属性
// lineData.push({ ...item1, ...item2 }); // 使用展开运算符合并对象,注意这可能会覆盖相同属性的值
// // 如果不希望覆盖,可以只添加item1或item2,或者创建一个新对象包含所需属性
// }
// });
// }
// });
// console.log('绘制降温的红圈和虚线lineData',lineData, coolData, allData)
// g.append('g')
// .selectAll('line')
// .data(lineData)
// .join('line')
// .attr('class', 'xuxianliane')
// .attr('x1', function({ index,date,value }) {
// return xScale(index) + viewConfig.X_OFFSET + (index - 20) / 4
// })
// .attr('y1', function({ value }) {
// return bodyScale(value)
// })
// .attr('x2', function({ index }) {
// return xScale(index) + viewConfig.X_OFFSET + (index - 20) / 4
// })
// .attr('y2', function({ index,date }) {
// const bodyValue = allData.filter(item => item.index === index && item.date === date )[0].value
// return bodyScale(bodyValue)
// })
// .attr('stroke', 'red')
// .attr('stroke-width', 2)
// .style('stroke-dasharray', '3, 3')
// .attr('stroke-linejoin', viewConfig.strokeLinejoin)
// .attr('stroke-linecap', viewConfig.strokeLinecap)
// // 绘制icon
// iconDrawObj.getDrawRoundIcon({
// content: g,
// data: d3.range(lineData.length),
// x: (i) => {
// return xScale(lineData[i].index) + viewConfig.X_OFFSET + (lineData[i].index - 20) / 4
// },
// y: (i) => {
// return bodyScale(lineData[i].value)
// },
// fill: 'transparent',
// stroke: 'red',
// r: 6
// })
// }
// 绘制降温的红圈和虚线
function drawCoolBody(svg, coolData, allData) {
const g = getG(svg, viewConfig)
// 获取同个位置记录的温度
const temArrMap = allData.reduce((data, items) => {
items.map((item) => {
if (item.value) {
data[item.index] = item.value
}
})
return data
}, {})
const vaildData = coolData.filter((item) => item.value)
const lineData = vaildData.filter(({ index, value }) => temArrMap[index] !== value)
g.append('g')
.selectAll('line')
.data(lineData)
.join('line')
.attr('class', 'xuxianliane')
.attr('x1', function({ index }) {
return xScale(index) + viewConfig.X_OFFSET
})
.attr('y1', function({ value }) {
return bodyScale(value)
})
.attr('x2', function({ index }) {
return xScale(index) + viewConfig.X_OFFSET
})
.attr('y2', function({ index,value }) {
const bodyValue = temArrMap[index]
let y2Location = bodyScale(value)
if (bodyValue !== undefined) {
y2Location = bodyScale(bodyValue)
}
return y2Location
})
.attr('stroke', 'red')
.attr('stroke-width', 2)
.style('stroke-dasharray', '3, 3')
.attr('stroke-linejoin', viewConfig.strokeLinejoin)
.attr('stroke-linecap', viewConfig.strokeLinecap)
// 绘制icon
iconDrawObj.getDrawRoundIcon({
content: g,
data: d3.range(vaildData.length),
x: (i) => {
return xScale(vaildData[i].index) + viewConfig.X_OFFSET
},
y: (i) => {
return bodyScale(vaildData[i].value)
},
fill: 'transparent',
stroke: 'red',
r: 6
})
}
// 绘制同一时间心率和脉搏的阴影
function drawRateBody(svg, pulseData, heartRateData) {
if (pulseData[0].length > 0 || heartRateData[0].length > 0) {
const g = getG(svg, viewConfig)
const defs = g.append("defs");
const pattern = defs.append("pattern")
.attr("id", "diagonal-hatch")
.attr("patternUnits", "userSpaceOnUse")
.attr("width", 1)
.attr("height", 6)
.attr("patternTransform", "rotate(100)"); // 45度斜线
pattern.append("path")
// .attr("d", "M-1,1 l2,-2 M0,4 l4,-4 M3,5 l2,-2")
.attr("d", "M-1,-1 l2,2 M0,0 l4,4 M3,3 l2,2")
.attr("stroke", 'red') // 虚线颜色
.attr("stroke-width", 1)
.attr("opacity", 0.8); // 透明度
// 创建一个映射,用于快速查找心率值
const heartRateMap = heartRateData[0].reduce((acc, curr) => {
acc[curr.index] = curr.value
return acc
}, {})
// 创建一个映射,用于快速查找脉搏值
const pulseDataMap = pulseData[0].reduce((acc, curr) => {
acc[curr.index] = curr.value
return acc
}, {})
if (Object.keys(heartRateMap).length >= 3 && Object.keys(pulseDataMap).length >= 3) {
// 获取脉搏和心率的起点和终点
const pulseStart = pulseData[0][0]
// const pulseEnd = pulseData[0][pulseData[0].length - 1]
const heartRateStart = heartRateMap[pulseStart.index]
// const heartRateEnd = heartRateMap[pulseEnd.index]
// 生成路径数据
const pathData = pulseData[0].reduce((acc, { value, index }, i) => {
const heartRateValue = heartRateMap[index]
// 过滤掉没有脉搏值和对应心率值的项
if (value !== null && value !== undefined && heartRateValue !== null && heartRateValue !== undefined) {
const y1 = heartScale(value || 0)
const x = xScale(index) + viewConfig.X_OFFSET
const y2 = heartScale(heartRateValue || 0)
// 如果是第一个点,移动到起始点
if (acc === '') {
acc += `M${x},${y1}` // Move to the starting point
}
acc += `L${x},${y2}` // Draw line to heart rate point
}
return acc
}, '')
// 生成闭合路径
let closingPath = pulseData[0].reduceRight((acc, { value, index }) => {
const heartRateValue = heartRateMap[index]
if (value !== null && value !== undefined && heartRateValue !== null && heartRateValue !== undefined) {
const x = xScale(index) + viewConfig.X_OFFSET
const y1 = heartScale(value || 0)
// const y2 = heartScale(heartRateValue)
acc += `L${x},${y1}` // Draw line down to pulse point
}
return acc
}, '') + `L${xScale(pulseStart.index) + viewConfig.X_OFFSET},${heartScale(heartRateStart)}` + 'Z' // Close the path
closingPath += `L${xScale(pulseStart.index) + viewConfig.X_OFFSET},${heartScale(heartRateStart)}Z`
// 绘制阴影区域
g.append('path')
.attr('class', 'rate-shade')
.attr('d', pathData + closingPath) // 完整的路径数据
.attr('fill', 'url(#diagonal-hatch)') // 使用图案填充
.attr('stroke', 'red') // 设置边框颜色为红色
}
}
}
// 绘制表格区域外面的信息数据
function drwaOtherInfo(svg, data) {
const g = svg
.append('g')
.attr('transform', `translate(${viewConfig.marginLeft},${viewConfig.marginTop})`)
.attr('style', 'font-size:14px;')
// 绘制第几周
getG(svg, viewConfig)
.append('text')
.attr('style', 'font-size:24px;')
.attr('class', 'mytext')
.text(`第${+data.weekNo + 1}周`)
.attr('x', viewConfig.width / 2 - 40)
.attr('y', viewConfig.bottomPos + TEXT_MARGIN_BOTTOM + 1.5 * LINE_HEIGHT - 25)
// 绘制标注
getG(svg, viewConfig)
.attr(
'transform',
`translate(${viewConfig.marginLeft},${viewConfig.bottomPos + viewConfig.marginTop + HEAD_HEIGHT})`
)
.attr('style', 'font-size:14px;')
.call((g) => {
const dataList = [
{
name: '体温',
fn: iconDrawObj.getDrawXIcon,
params: {
content: g,
data: [0]
}
},
{
name: '心率',
fn: iconDrawObj.getDrawRoundIcon,
params: {
content: g,
data: [0],
fill: 'white',
stroke: 'red'
}
},
{
name: '脉搏',
fn: iconDrawObj.getDrawRoundIcon,
params: {
content: g,
data: [0],
fill: 'red',
stroke: 'red'
}
}
]
g.append('text').text('标注:')
dataList.map((item, i) => {
g.append('text')
.text(item.name)
.attr('x', 40 + i * 80)
item.fn({
...item.params,
x: () => 80 + i * 80,
y: () => -4
})
})
})
.attr('x', viewConfig.width / 2 - 40)
.attr('y', viewConfig.bottomPos + TEXT_MARGIN_BOTTOM)
// 绘制顶部的信息数据
g.append('text')
.attr('class', 'mytext')
.attr('x', 0)
.attr('y', () => {
return HEAD_HEIGHT - TEXT_MARGIN_BOTTOM - 6
})
.html(() => {
return INFO_KEYS.map(({ name, key }, index) => {
return `${name}: ${data[key] ? data[key] : ''}`
}).join('')
})
// 绘制标题
g.append('text')
.attr('style', 'font-size:22px;text-anchor: middle;')
.attr('class', 'mytext')
.attr('x', viewConfig.width / 2)
.attr('y', () => {
return HEAD_HEIGHT - 4 * LINE_HEIGHT
})
.text(Header.HospitalName)
// 体温单
g.append('text')
.attr('style', 'font-size:22px;text-anchor: middle;')
.attr('class', 'mytext')
.attr('x', viewConfig.width / 2)
.attr('y', () => {
return HEAD_HEIGHT - 2.5 * LINE_HEIGHT
})
.text(Header.temperatureName)
}
function generatePointer({ pathData, type, yScaleInstance }) {
return (event) => {
var index = Math.round((d3.pointer(event)[0] - viewConfig.step - textLeftMargin) / xScale.step())
var val = xScale.domain()[index]
// const i = d3.bisectCenter(d3.range(pathData.length), val);
let i = -1
pathData.forEach((item, index) => {
if (item.index === val) {
i = index
}
})
const yPos = yScaleInstance(+pathData[i].value) + viewConfig.marginTop + HEAD_HEIGHT
tooltip.style('display', null)
tooltip.attr('class', 'myTooltip')
tooltip.attr('transform', `translate(${xScale(val) + viewConfig.micoStep},${yPos})`)
const path = tooltip.selectAll('path').data(['', '']).join('path').attr('fill', 'white').attr('stroke', 'black')
const text = tooltip
.selectAll('text')
.data(['', ''])
.join('text')
.call((text) =>
text
.selectAll('tspan')
.data([`${type}: ${pathData[i].value}`, `${pathData[i].date}`])
.join('tspan')
.attr('x', 0)
.attr('y', (_, i) => `${i * 1.2}em`)
.attr('font-weight', (_, i) => (i ? null : 'bold'))
.text((d) => d)
)
const { y, width: w, height: h } = text.node().getBBox()
text.attr('transform', `translate(${-w / 2},${15 - y})`)
path.attr('d', `M${-w / 2 - 10},5H-5l5,-5l5,5H${w / 2 + 10}v${h + 20}h-${w + 20}z`)
}
}
function pointerleft() {
tooltip.style('display', 'none')
}
}
function getTypeValue(type, allData = [], isNumber = true) {
return allData.filter((item) => item.typeCode === type || '')
}