Files
wx_pbzc/components/trend-chart/trend-chart.js
T
lc ca1793b4c0 feat: 个人最佳趋势改为折线图 + 底部最佳趋势日期 + 修复
- 趋势组件 trend-chart 新增 chartType(line/bar),line 模式用内联 SVG 渲染折线+渐变面积(无 canvas)
- 记录页个人最佳趋势改用折线图,峰值信息移到图表下方对称高亮块显示最佳趋势产生日期
- 修复1: linePoints 漏存绝对坐标 y 导致运行时 toFixed 崩溃
- 修复2: 折线模式漏渲染数据点数值(valueText),现圆点上方显示且 0 值不显示
2026-08-04 15:46:18 +08:00

109 lines
4.1 KiB
JavaScript

const { getBarPercent } = require('../../utils/util')
/**
* Trend chart — pure CSS bar mode + optional SVG line mode (no canvas),
* matching the calendar-heatmap's rendering approach for consistency.
*
* chartData: [{ label, value, valueText, highlight }]
* - label: bottom caption (e.g. "一" / "7月")
* - value: numeric magnitude (seconds); drives bar height / line y
* - valueText: formatted string shown above the bar (e.g. "1分30秒")
* - highlight: truthy -> theme glow + bold label (today / this month)
*
* chartType: 'bar' (default) | 'line'
* - bar: CSS columns, height strictly proportional to value/max.
* - line: inline SVG (data URI) polyline + translucent area, rendered in an
* <image>; dots + axis labels are real WXML elements positioned by
* percentage so they stay crisp and theme-colored (SVG inside <image>
* cannot read page CSS variables). The viewBox aspect is matched to
* the container's rpx aspect (widthFix) so the stroke stays uniform
* — no non-uniform horizontal/vertical stretch.
*
* lineColor: hex used for the SVG stroke + area fill. Must be passed by the
* page because SVG inside <image> cannot read page CSS variables.
*/
const VB_W = 640
Component({
properties: {
chartData: { type: Array, value: [] },
variant: { type: String, value: 'total' },
chartType: { type: String, value: 'bar' },
lineColor: { type: String, value: '#FF6B35' }
},
data: {
bars: [],
linePoints: [],
lineSvg: ''
},
observers: {
'chartData, chartType, lineColor'(chartData, chartType, lineColor) {
this._compute(chartData, chartType, lineColor)
}
},
lifetimes: {
ready() {
this._compute(this.properties.chartData, this.properties.chartType, this.properties.lineColor)
}
},
methods: {
_compute(chartData, chartType, lineColor) {
const arr = Array.isArray(chartData) ? chartData : []
const max = arr.reduce((m, d) => Math.max(m, Number(d && d.value) || 0), 0)
const bars = arr.map((d) => {
const v = Number(d && d.value) || 0
return {
label: (d && d.label) || '',
value: v,
valueText: (d && d.valueText) || '',
highlight: !!(d && d.highlight),
percent: getBarPercent(v, max)
}
})
const N = arr.length
let linePoints = []
let lineSvg = ''
if (N > 0 && chartType === 'line') {
const lineH = this.properties.variant === 'peak' ? 170 : 200
const topPad = 16
const bottomPad = 12
const plot = lineH - topPad - bottomPad
linePoints = bars.map((b, i) => {
const x = (i + 0.5) * (VB_W / N)
const y = topPad + (1 - b.percent / 100) * plot
return {
x,
y,
xPercent: (x / VB_W) * 100,
yPercent: (y / lineH) * 100,
label: b.label,
valueText: b.valueText,
value: b.value,
highlight: b.highlight
}
})
lineSvg = this._buildLineSvg(linePoints, VB_W, lineH, lineColor || '#FF6B35')
}
this.setData({ bars, linePoints, lineSvg })
},
_buildLineSvg(points, W, H, color) {
const valid = (points || []).filter(p => p && typeof p.x === 'number' && typeof p.y === 'number')
if (valid.length === 0) return ''
const pts = valid.map(p => `${p.x.toFixed(2)},${p.y.toFixed(2)}`).join(' ')
const first = valid[0]
const last = valid[valid.length - 1]
let area = `M ${first.x.toFixed(2)},${H} `
valid.forEach((p) => { area += `L ${p.x.toFixed(2)},${p.y.toFixed(2)} ` })
area += `L ${last.x.toFixed(2)},${H} Z`
const svg =
`<svg xmlns="http://www.w3.org/2000/svg" width="${W}" height="${H}" viewBox="0 0 ${W} ${H}" preserveAspectRatio="none">` +
`<path d="${area}" fill="${color}" fill-opacity="0.16"/>` +
`<polyline points="${pts}" fill="none" stroke="${color}" stroke-width="5" stroke-linejoin="round" stroke-linecap="round"/>` +
`</svg>`
return 'data:image/svg+xml;charset=utf-8,' + encodeURIComponent(svg)
}
}
})