feat: 个人最佳趋势改为折线图 + 底部最佳趋势日期 + 修复
- 趋势组件 trend-chart 新增 chartType(line/bar),line 模式用内联 SVG 渲染折线+渐变面积(无 canvas) - 记录页个人最佳趋势改用折线图,峰值信息移到图表下方对称高亮块显示最佳趋势产生日期 - 修复1: linePoints 漏存绝对坐标 y 导致运行时 toFixed 崩溃 - 修复2: 折线模式漏渲染数据点数值(valueText),现圆点上方显示且 0 值不显示
This commit is contained in:
@@ -1,38 +1,53 @@
|
||||
const { getBarPercent } = require('../../utils/util')
|
||||
|
||||
/**
|
||||
* Trend bar chart - pure CSS (no canvas), matches the calendar-heatmap's
|
||||
* rendering approach for consistency.
|
||||
* 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
|
||||
* - 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)
|
||||
*
|
||||
* Bar height is strictly proportional to value/max. Zero values render a
|
||||
* faint stub so empty dates remain visible without distorting the data.
|
||||
* 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' }
|
||||
variant: { type: String, value: 'total' },
|
||||
chartType: { type: String, value: 'bar' },
|
||||
lineColor: { type: String, value: '#FF6B35' }
|
||||
},
|
||||
data: {
|
||||
bars: []
|
||||
bars: [],
|
||||
linePoints: [],
|
||||
lineSvg: ''
|
||||
},
|
||||
observers: {
|
||||
'chartData'(chartData) {
|
||||
this._compute(chartData)
|
||||
'chartData, chartType, lineColor'(chartData, chartType, lineColor) {
|
||||
this._compute(chartData, chartType, lineColor)
|
||||
}
|
||||
},
|
||||
lifetimes: {
|
||||
ready() {
|
||||
this._compute(this.properties.chartData)
|
||||
this._compute(this.properties.chartData, this.properties.chartType, this.properties.lineColor)
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
_compute(chartData) {
|
||||
_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) => {
|
||||
@@ -45,7 +60,49 @@ Component({
|
||||
percent: getBarPercent(v, max)
|
||||
}
|
||||
})
|
||||
this.setData({ bars })
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user