48 情绪折线图:EmotionTrendChart 完整实现

前言

在这里插入图片描述

图:48 情绪折线图:EmotionTrendChart 完整实现 运行效果截图(HarmonyOS NEXT)

除了雷达图,情绪趋势折线图是 App 中另一个核心 Canvas 可视化组件。它在 TrendPage(趋势页)和 个人中心 中展示用户近 30 天的情绪能量变化。

本文以 [EmotionTrendChart.ets](file:///Users/fiona/Downloads/bijixinli/harmony-app/entry/src/main/ets/common/components/EmotionTrendChart.ets) 为例,完整解析 Canvas 折线图的实现——从坐标映射、渐变填充到逐段绘制动画。

鸿蒙官方·Canvas 渐变文档:developer.huawei.com
项目源码:[EmotionTrendChart.ets](file:///Users/fiona/Downloads/bijixinli/harmony-app/entry/src/main/ets/common/components/EmotionTrendChart.ets)

情绪折线图绘制流程示意图

图:EmotionTrendChart 绘制流程——坐标映射 → 渐变填充区域 → 折线描边 → 数据点标记

30天情绪数据

坐标映射
时间轴 × 分值轴

绘制背景渐变区域
createLinearGradient

绘制折线
moveTo/lineTo

绘制数据点
arc 小圆点

绘制 X 轴标签
日期文字

入场动画
逐段绘制 progress

一、图表整体结构

1.1 最终效果

   100 ──────────────────────────────
    75 ──────────╮──────────────────
    50 ─────╮────╯──────╮───────────
    25 ─────╯───────────╯───────────
     0 ──────────────────────────────
       6/01     6/15      6/30

   ┌──────┐ ┌──────┐ ┌──────┐ ┌────┐
   │ 暖色  │ │ 静色  │ │ 柔色  │ │ 静 │  ← 四色情绪区
   └──────┘ └──────┘ └──────┘ └────┘

1.2 数据接口

export interface TrendPoint {
  label: string   // 横轴标签,如 "6/01"
  value: number   // 0-100 的情绪能量值
}

// 实际数据示例
this.trendData = [
  { label: '6/01', value: 62 },
  { label: '6/03', value: 55 },
  { label: '6/05', value: 68 },
  // ...
  { label: '6/30', value: 72 }
]

二、布局与坐标系

2.1 内边距与绘图区

const W = this.chartWidth     // 画布宽度 320
const H = this.chartHeight    // 画布高度 180

// 四边留白
const padL = 28   // 左:纵轴刻度
const padR = 14   // 右:留白
const padT = 14   // 上:留白
const padB = 24   // 下:横轴标签

// 内部绘图区(实际绘图范围)
const innerW = W - padL - padR  // 320-28-14 = 278
const innerH = H - padT - padB  // 180-14-24 = 142

2.2 坐标映射

// 数据点坐标转换
const stepX = innerW / Math.max(1, this.data.length - 1)
// stepX:相邻数据点的水平间距

const allPoints: ChartPoint[] = this.data.map((p, i): ChartPoint => ({
  x: padL + stepX * i,              // X = 左边界 + 间距 × 索引
  y: padT + innerH * (1 - p.value / 100)  // Y = 顶部 + 高度 × (1-数值比例)
}))

Y 轴公式解释:

// 因为 Canvas Y 轴向下为正,所以:
// value=100 → 图表顶部(padT)
// value=0   → 图表底部(padT + innerH)
// 即:y = padT + innerH * (1 - value/100)

三、情绪色块背景

3.1 四色分区

图表背景被分为四个色块区域,对应情绪的四种状态:

private static readonly ZONES: Zone[] = [
  { w: 0.200, fill: '#F0E1D0' },   // 暖色
  { w: 0.267, fill: '#E1E8E5' },   // 静色
  { w: 0.233, fill: '#E8DCDC' },   // 柔色
  { w: 0.300, fill: '#E1E8E5' }    // 静色
]

3.2 绘制代码

// 绘制四色背景块
let zx = padL
this.zones.forEach(z => {
  const zw = Math.round(z.w * innerW)    // 色块宽度 = 绘图区宽度 × 比例
  ctx.fillStyle = z.fill
  ctx.globalAlpha = 0.5                  // 50% 透明度
  ctx.fillRect(zx, padT, zw, innerH)     // 填充矩形
  zx += zw                                // 移动到下一个色块起始位置
})
ctx.globalAlpha = 1                       // 恢复透明度
色块 比例 颜色 情绪含义
暖色 20.0% #F0E1D0 积极/温暖
静色 26.7% #E1E8E5 平静/中性
柔色 23.3% #E8DCDC 柔和/犹豫
静色 30.0% #E1E8E5 平静/中性

四、参考线与纵轴刻度

4.1 虚线参考线

4 条水平虚线将图表 5 等分(0, 25, 50, 75, 100):

// 设置虚线样式
ctx.strokeStyle = AppColors.LINE
ctx.lineWidth = 1
ctx.setLineDash([4, 4])     // 实线 4px,间隔 4px

for (let i = 0; i <= 4; i++) {
  const y = padT + (innerH * i) / 4     // 等分 Y 坐标
  ctx.beginPath()
  ctx.moveTo(padL, y)                    // 从左边界开始
  ctx.lineTo(padL + innerW, y)           // 到右边界结束
  ctx.stroke()
}

ctx.setLineDash([])                      // 恢复实线

4.2 纵轴刻度标签

ctx.fillStyle = AppColors.TEXT_3
ctx.font = '10px HarmonyOS Sans SC'
ctx.textAlign = 'right'
ctx.textBaseline = 'middle'

const ticks = [100, 75, 50, 25, 0]       // 5 个刻度值
ticks.forEach((t, i) => {
  const y = padT + (innerH * i) / 4
  ctx.fillText(`${t}`, padL - 6, y)      // 刻度在参考线左侧 6px
})

五、折线主体绘制

5.1 从左到右逐段绘制

通过 drawProgress 控制可见数据点的数量,实现从左到右的"逐段绘制"动画:

// 计算当前可见点数
const visibleCount = Math.floor(this.drawProgress * this.data.length)
if (visibleCount === 0) return

// 截取可见点
const points = allPoints.slice(0, visibleCount)
const lastPoint = points[points.length - 1]

5.2 渐变填充

在折线下方创建渐变填充区域:

// 创建垂直渐变
const grad = ctx.createLinearGradient(0, padT, 0, padT + innerH)
grad.addColorStop(0, 'rgba(168,144,122,0.32)')    // 顶部较深
grad.addColorStop(1, 'rgba(168,144,122,0.02)')    // 底部几乎透明

// 构建填充路径(折线 + 底部闭合)
ctx.beginPath()
ctx.moveTo(allPoints[0].x, padT + innerH)          // 从左下角开始
points.forEach((p) => ctx.lineTo(p.x, p.y))        // 沿数据点向上
ctx.lineTo(lastPoint.x, padT + innerH)              // 回到底部
ctx.closePath()
ctx.fillStyle = grad
ctx.fill()

填充路径示意图:

  ╱╲    ╱╲
 ╱  ╲  ╱  ╲
╱    ╲╱    ╲____________________   ← 沿底部闭合

5.3 主轨迹线

ctx.beginPath()
points.forEach((p, i) => {
  if (i === 0) ctx.moveTo(p.x, p.y)
  else ctx.lineTo(p.x, p.y)
})
ctx.strokeStyle = AppColors.PRIMARY    // 品牌色 #A8907A
ctx.lineWidth = 2
ctx.stroke()

5.4 数据点

points.forEach((p, i) => {
  ctx.beginPath()
  // 最后一个数据点更大(highlight),且仅当动画完成时
  ctx.arc(
    p.x, p.y,
    i === allPoints.length - 1 && this.drawProgress >= 1 ? 4 : 2.5,
    0, Math.PI * 2
  )
  ctx.fillStyle = i === allPoints.length - 1 && this.drawProgress >= 1
    ? AppColors.PRIMARY_DEEP    // 最后一个点深色突出
    : AppColors.PRIMARY          // 普通点品牌色
  ctx.fill()
})

数据点大小规则:

条件 半径 颜色
普通点 2.5px PRIMARY
最后一个点(动画未完成) 2.5px PRIMARY
最后一个点(动画已完成) 4px PRIMARY_DEEP

六、横轴标签

6.1 仅显示首/中/末三个标签

为避免拥挤,只显示 3 个横轴标签:

// 只显示首、中、尾三个标签
const labelIdx = [0, Math.floor(this.data.length / 2), this.data.length - 1]

ctx.fillStyle = AppColors.TEXT_3
ctx.textAlign = 'center'
ctx.textBaseline = 'top'

labelIdx.forEach((i) => {
  // 只在可见范围内的标签才绘制
  if (this.data[i] && i < visibleCount) {
    ctx.fillText(this.data[i].label, allPoints[i].x, padT + innerH + 6)
  }
})

6.2 为什么只显示三个标签?

方案 效果
全部显示 30 天 × 每天一个标签 → 重叠无法阅读 ❌
指定间隔 如每隔 5 天显示 → 逻辑复杂
首/中/末三个 清晰、简洁、足够表达时间范围 ✅

七、完整的绘制流程

7.1 绘制顺序

第 1 层:四色情绪背景块(fillRect)
第 2 层:虚线参考线 × 4(setLineDash)
第 3 层:纵轴刻度 × 5(fillText)
第 4 层:渐变填充区域(createLinearGradient + fill)
第 5 层:主折线轨迹(lineWidth=2 + stroke)
第 6 层:数据点(arc + fill)
第 7 层:横轴标签(fillText × 3)

7.2 绘制函数完整结构

private draw(): void {
  // 1. 清空画布
  ctx.clearRect(0, 0, W, H)

  // 2. 四色背景区
  this.drawZones()

  // 3. 参考线 + 纵轴
  this.drawGridLines()
  this.drawYTicks()

  // 4. 数据折线(渐变填充 + 轨迹 + 数据点)
  if (this.data.length === 0) return
  this.drawLine()

  // 5. 横轴标签
  this.drawXLabels()
}

八、动画整合

8.1 从左到右的绘制动画

// aboutToAppear 触发动画
aboutToAppear(): void {
  animateTo({
    duration: AppAnimations.CHART_DRAW_DURATION,  // 1200ms
    curve: AppAnimations.CURVE_SMOOTH
  }, () => {
    this.drawProgress = 1
  })
}

// @Watch 监听
@State @Watch('onDrawProgressChange') drawProgress: number = 0

onDrawProgressChange(): void {
  this.draw()
}

// draw() 中使用 drawProgress
const visibleCount = Math.floor(this.drawProgress * this.data.length)
// visibleCount 从 0 逐渐增长到 data.length
// 从而实现从左到右逐段绘制

8.2 动画时序

T=0ms     drawProgress=0,    visibleCount=0    → 空画布
T=300ms   drawProgress=0.25, visibleCount=8    → 前 8 个数据点
T=600ms   drawProgress=0.5,  visibleCount=15   → 一半数据
T=900ms   drawProgress=0.75, visibleCount=23   → 大部分
T=1200ms  drawProgress=1,    visibleCount=30   → 完成,最后一个点放大

九、复用情况

EmotionTrendChart 在两个页面中复用:

9.1 TrendPage(趋势页)

// TrendPage.ets — 展示 30 天情绪趋势
EmotionTrendChart({
  data: this.trendData,
  chartWidth: 320,
  chartHeight: 180
})

9.2 个人中心(统计概览)

// MePage.ets — 最近 7 天概览(同组件,不同尺寸)
EmotionTrendChart({
  data: this.weeklyData,
  chartWidth: 260,
  chartHeight: 140
})

9.3 设计要点总结

特性 实现 说明
渐变填充 createLinearGradient 顶部 rgba(168,144,122,0.32) → 底部 0.02
虚线参考线 setLineDash([4,4]) 4 条水平线,5 等分
逐段动画 drawProgress × data.length 从左到右逐点显现
最后一个点高亮 半径 4px + 深色 指示"当前"位置
纵轴刻度 手动计算 5 个刻度 100/75/50/25/0
横轴标签 首/中/末三个 避免拥挤

总结

本文完整解析了 EmotionTrendChart 情绪折线图的实现:

  1. 布局系统:四边留白(padL/padR/padT/padB)+ 内部绘图区
  2. 色块背景:四色情绪分区(暖/静/柔/静),比例共 100%
  3. 参考系统:4 条虚线参考线 + 5 个纵轴刻度
  4. 折线绘制:渐变填充底部 + 主轨迹线 + 数据点
  5. 动画实现drawProgress 控制可见点数 → 从左到右逐段绘制

下一篇文章将介绍 MatchRing 匹配度圆环绘制——双人合盘匹配度的环形进度图。

如果这篇文章对你有帮助,欢迎点赞👍、收藏⭐、关注🔔,你的支持是我持续创作的动力!


参考资源:

  • createLinearGradient 渐变
  • setLineDash 虚线设置
  • arc 弧线绘制
  • [EmotionTrendChart 组件源码](file:///Users/fiona/Downloads/bijixinli/harmony-app/entry/src/main/ets/common/components/EmotionTrendChart.ets)
  • [TrendPage 趋势页源码](file:///Users/fiona/Downloads/bijixinli/harmony-app/entry/src/main/ets/pages/TrendPage.ets)
  • [AppAnimations 动画常量](file:///Users/fiona/Downloads/bijixinli/harmony-app/entry/src/main/ets/common/theme/AppAnimations.ets)
  • HarmonyOS 开发文档
Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐