Files
his/openhis-ui-vue3/src/action/nurseStation/temperatureSheet/line.js
2025-09-02 19:16:04 +08:00

1697 lines
58 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 `<tspan>${value[0]}</tspan><tspan dx="${-35 + index * 10}" dy="15">${value[1]}</tspan><tspan dx="${
-35 + index * 10
}" dy="15">${value[2]}</tspan>`
}
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 `<tspan dx="10" dy="10">${value[0]}</tspan><tspan dx="${-15 + index * 10}" dy="25">${value[1]}</tspan><tspan dx="${-15 + index * 10}" dy="25">${value[2]}</tspan><tspan dx="${-15 + index * 10}" dy="25">${value[3]}</tspan>`
} else if (i === 0) {
return `<tspan dx="10">${d}</tspan>`
}
return `<tspan dx="4">${d}</tspan>`
})
.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 `<tspan dx="${index === 0 ? 0 : 40}" dy="${0}">${name}: ${data[key] ? data[key] : ''}</tspan>`
}).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 || '')
}