根据S-T教学分析法绘制图形-前端实现
最近有一个需求,就是根据S-T教学分析法得到的相关数据,比如:[[0,0],[0,1],[1.1],[1,2]],最后一个元素中的x,y之和就是整堂课的总时长,最终会得到一个图,由这个图可以看出是这一堂课是学生主导还是教师主导,大致就是这个意思,可以参考S-T教学分析法这篇文章。
此篇文章主要就是记录一下实现这种类似得图形如何绘制,之前想过直接使用ECharts中得折线图,然后设置series
中的step
属性,发现最终的效果始终不满足上面文章概念中的那种效果,于是就使用canvas手绘了一个类似的。
效果

直接上代码
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Animated Line Graph</title>
<style>
canvas {
border: 1px solid #ccc;
margin: 20px;
}
</style>
</head>
<body>
<canvas id="myCanvas" width="600" height="400"></canvas>
<script>
const canvas = document.getElementById('myCanvas');
const ctx = canvas.getContext('2d');
const dpr = window.devicePixelRatio
ctx.imageSmoothingEnabled = true // 启用抗锯齿
ctx.scale(dpr, dpr)
canvas.style.width = canvas.clientWidth + 'px'
canvas.style.width = canvas.clientHeight + 'px'
canvas.width = Math.round(canvas.clientWidth * dpr)
canvas.height = Math.round(canvas.clientHeight * dpr)
const margin = { top: 20, right: 20, bottom: 40, left: 30 }
const width = canvas.width - margin.left - margin.right
const height = canvas.height - margin.top - margin.bottom
const textFont = '12px Philosopher-Regular'
const strokeStyle = '#333'
const gridLineColor = `rgba(118, 118, 118, 0.5)`
const tooltipInfo = null // 存储tooltip信息
const activeGridLine = null // 存储当前激活的网格线索引
const data = [{ "x": 0, "y": 0 }, { "x": 0, "y": 1 }, { "x": 0, "y": 2 }, { "x": 0, "y": 3 }, { "x": 0, "y": 4 }, { "x": 1, "y": 4 }, { "x": 2, "y": 4 }, { "x": 2, "y": 5 }, { "x": 2, "y": 6 }, { "x": 3, "y": 6 }, { "x": 3, "y": 7 }, { "x": 3, "y": 8 }, { "x": 4, "y": 8 }, { "x": 4, "y": 9 }, { "x": 4, "y": 10 }, { "x": 4, "y": 11 }, { "x": 4, "y": 12 }, { "x": 5, "y": 12 }, { "x": 6, "y": 12 }, { "x": 7, "y": 12 }, { "x": 7, "y": 13 }, { "x": 7, "y": 14 }, { "x": 7, "y": 15 }, { "x": 7, "y": 16 }, { "x": 8, "y": 16 }, { "x": 8, "y": 17 }, { "x": 8, "y": 18 }, { "x": 8, "y": 19 }, { "x": 9, "y": 19 }, { "x": 9, "y": 20 }, { "x": 9, "y": 21 }, { "x": 10, "y": 21 }, { "x": 10, "y": 22 }, { "x": 10, "y": 23 }, { "x": 10, "y": 24 }, { "x": 10, "y": 25 }, { "x": 11, "y": 25 }, { "x": 12, "y": 25 }, { "x": 13, "y": 25 }, { "x": 13, "y": 26 }, { "x": 13, "y": 27 }, { "x": 13, "y": 28 }, { "x": 13, "y": 29 }]
const max = data.length;
const gridCountX = Math.ceil(max);
const gridCountY = Math.ceil(max);
let animationProgress = 0;
let animationDuration = 2000;
let startTime = null;
// 绘制函数
function drawGraph(timestamp) {
if (!startTime) startTime = timestamp;
const elapsed = timestamp - startTime;
animationProgress = Math.min(elapsed / animationDuration, 1);
ctx.clearRect(0, 0, canvas.width, canvas.height);
// 绘制坐标轴
ctx.beginPath();
ctx.moveTo(margin.left, margin.top);
ctx.lineTo(margin.left, margin.top + height);
ctx.lineTo(margin.left + width, margin.top + height);
ctx.strokeStyle = strokeStyle;
ctx.lineWidth = 2;
ctx.stroke();
// 绘制网格线(带淡入动画)
for (let i = 0; i <= gridCountX; i++) {
// 这里可以控制垂直网格线的数量
if (i % 5 === 0) {
const x = margin.left + (i / gridCountX) * width;
let alpha = animationProgress * 0.3
if (i === 0) {
alpha = animationProgress * 0.6
}
// 垂直网格线
ctx.beginPath();
ctx.moveTo(x, margin.top);
ctx.lineTo(x, margin.top + height);
ctx.strokeStyle = `rgba(118, 118, 118, ${alpha})`;
ctx.lineWidth = 1;
ctx.stroke();
// X轴刻度
ctx.beginPath();
ctx.moveTo(x, margin.top + height);
ctx.lineTo(x, margin.top + height + 5);
ctx.strokeStyle = strokeStyle;
ctx.stroke();
ctx.fillStyle = strokeStyle;
ctx.font = textFont;
ctx.textAlign = 'center';
ctx.fillText(i, x, margin.top + height + 20);
}
}
for (let i = 0; i <= gridCountY; i++) {
// 这里可以控制水平网格线的数量
if (i % 5 === 0) {
const y = margin.top + height - (i / gridCountY) * height;
const alpha = animationProgress;
// 水平网格线
ctx.beginPath();
ctx.moveTo(margin.left, y);
ctx.lineTo(margin.left + width, y);
ctx.strokeStyle = `rgba(118, 118, 118, ${alpha})`;
ctx.lineWidth = 1;
ctx.stroke();
// Y轴刻度
ctx.beginPath();
ctx.moveTo(margin.left, y);
ctx.lineTo(margin.left - 5, y);
ctx.strokeStyle = strokeStyle;
ctx.stroke();
ctx.fillStyle = strokeStyle;
ctx.font = textFont;
ctx.textAlign = 'right';
ctx.textBaseline = 'middle';
ctx.fillText(i, margin.left - 10, y);
}
}
// 绘制单位
ctx.fillStyle = strokeStyle
ctx.font = '12px Arial'
ctx.textAlign = 'left'
ctx.fillText('T', margin.left + width + 10, margin.top + height + 12)
ctx.fillText('S', margin.left - 20, margin.top - 10)
// 绘制连接线(带绘制动画)
if (animationProgress > 0) {
ctx.beginPath()
// 计算需要绘制的线段数
const totalSegments = data.length - 1
const segmentsToDraw = Math.ceil(totalSegments * animationProgress)
// 如果有线段需要绘制
if (segmentsToDraw > 0) {
// 绘制完整线段
for (let i = 0; i < segmentsToDraw - 1; i++) {
const p1 = data[i]
const p2 = data[i + 1]
const x1 = margin.left + (p1.x / max) * width
const y1 = margin.top + height - (p1.y / max) * height
const x2 = margin.left + (p2.x / max) * width
const y2 = margin.top + height - (p2.y / max) * height
if (i === 0) {
ctx.moveTo(x1, y1)
}
ctx.lineTo(x2, y2)
}
// 绘制部分线段(动画进行中)
if (segmentsToDraw < data.length) {
const p1 = data[segmentsToDraw - 1]
const p2 = data[segmentsToDraw]
const x1 = margin.left + (p1.x / max) * width
const y1 = margin.top + height - (p1.y / max) * height
const x2 = margin.left + (p2.x / max) * width
const y2 = margin.top + height - (p2.y / max) * height
// 计算部分线段的终点
const partialProgress = animationProgress * totalSegments - (segmentsToDraw - 1)
const partialX = x1 + (x2 - x1) * partialProgress
const partialY = y1 + (y2 - y1) * partialProgress
if (segmentsToDraw - 1 === 0) {
ctx.moveTo(x1, y1)
}
ctx.lineTo(partialX, partialY)
}
ctx.strokeStyle = '#38C6FD'
ctx.lineWidth = 2
ctx.stroke()
}
}
// 继续动画循环
if (animationProgress < 1) {
requestAnimationFrame(drawGraph)
}
}
// 初始化绘制
requestAnimationFrame(drawGraph);
</script>
</body>
</html>
总结
上面的代码可能需要一定的canvas绘画的基础,逻辑其实不算复杂。反正鄙人是全网没有找到相关内容的文章,所以只能手绘,如果大家有更好的办法,请分享!
狗头保命(又水一篇)