React-Native开发鸿蒙NEXT-svg绘制睡眠质量图part1
FBI WARNING:
The complete demo will be posted at the end of the series, so no need to worry.
上的个逼班,做的是个蓝牙戒指。好几个厂家的样品测了一阵,算是选中了一个开始对接。

有个功能是监测睡眠,睡眠数据会生成一张图。

这图有说叫睡眠阶段图的,有说叫睡眠质量图的,没找到权威的说法。看过好几家戒指的App,几乎都做的差不多,感觉上是一个现成的三方,或者是某个图表组件中的一个功能。找了一阵,可能就是因为不知道它的正式名称,一直没找到对应的三方组件。单论图表组件,Victory和ECharts里都没找到,这俩库都找不到合适的,感觉这么找下去有点悬,还是先出个方案开发吧。于是就想到了用svg去画一下。毕竟react-native-svg鸿蒙也支持,后续即便找到三方,也不一定能鸿蒙化,反而不美。之前从没做过图表,这玩意一般都让前端去开发,这次也是临时学了下。
首先科普下睡眠质量,这个倒是有医学上的定义的---按照阶段可以分为深睡眠,浅睡眠,快速眼动,清醒这四个阶段。定义一个最简单的模型
typescript
enum SleepStageEnum {
Deep = 0, // 0 深睡眠
Light, // 1 浅睡眠
REM, // 2 快速眼动
AWAKE, // 3 清醒
}
我们不关心戒指怎么去得到这些数据,只要知道数据的采集频率,比如每隔五分钟采集一次数据,反馈当前的睡眠状态。所以数据上可以用采样时间+当前睡眠状态来模拟。
typescript
// 示例数据
const sleepData: SleepStageModel[] = [
{time: '0:00', stage: 3},
{time: '5:00', stage: 3},
{time: '10:00', stage: 1},
{time: '15:00', stage: 1},
{time: '20:00', stage: 3},
{time: '25:00', stage: 0},
{time: '30:00', stage: 0},
{time: '35:00', stage: 0},
{time: '40:00', stage: 0},
{time: '45:00', stage: 1},
{time: '50:00', stage: 1},
{time: '55:00', stage: 3},
{time: '60:00', stage: 3},
{time: '65:00', stage: 2},
{time: '70:00', stage: 2},
{time: '75:00', stage: 1},
{time: '80:00', stage: 3},
{time: '85:00', stage: 2},
{time: '0:00', stage: 3},
{time: '5:00', stage: 3},
{time: '10:00', stage: 1},
{time: '15:00', stage: 1},
{time: '20:00', stage: 3},
{time: '25:00', stage: 0},
// ...更多数据
];
现在回过头来看看这个图表,看着有点科技,其实扒光效果放到excel里就类似这挫样。其中画红圈的地方是采样数据点。

可以看到,阶段的"面积变化"是由当前点和它前一个点的状态决定的。两者不一致则意味着上一个阶段结束,下一个阶段开始。这是绘制图表逻辑上最重要的地方。
react-native-svg的坐标系,原点位于左上角,横轴x纵轴y,知道坐标系就可以设计将模拟数据"摆放"到坐标系相应位置上。先定义一些辅助常量(因为是整个demo做完了才开始码字,代码难免和当前描述相比有点超前,见谅)
typescript
// 组件目前默认按照屏幕宽度为基准进行布局
const {width: SCREEN_WIDTH} = Dimensions.get('window');
const SHOW_DATA_POINT = true; // 是否在图标展示数据点(调试阶段可以更清晰核对数据与图是否一致)
const MARGIN = 24; // 左右间隙,用于支持底部指针图标的左右拖拽
const POINT_RADIUS = 4; // 数据点的弧度
const AREA_HEIGHT = 30; // 阶段形状的高度
const CHART_HEIGHT = AREA_HEIGHT * 9; // 图表的整体高度,可以调整
// const AREA_LINE_HEIGHT = 20; // 阶段形状线的高度
将模拟数据转换成点模型
typescript
const points = useMemo(
() =>
data.map((item, index) => ({
x: (index * (SCREEN_WIDTH - MARGIN * 2)) / (data.length - 1) + MARGIN,
y: CHART_HEIGHT - (item.stage * 2 + 1) * AREA_HEIGHT - AREA_HEIGHT,
stage: item.stage,
data: item,
})),
[data],
);
这时候,已经可以利用svg绘制出这些点了。svg中可以用Circle来绘制圆圈。通过遍历points即可将这些点用圆圈绘制到图表中。
typescript
<Svg height={CHART_HEIGHT + 80} width={SCREEN_WIDTH}>
points.map((point, index) => (
<G key={`point-${index}`}>
<Circle
cx={point.x}
cy={point.y}
r={POINT_RADIUS}
fill={STAGE_CONFIG[point.stage].color}
stroke="white"
strokeWidth={1}
/>
</G>
</Svg>
看看此时的效果!

有点简陋,先加上些辅助信息。比如加点表格线,比如加点说明。
svg中,可以通过Line来添加线段。
typescript
// 添加绘制参考线的函数
const renderReferenceLines = () => {
const lines: JSX.Element[] = [];
// 计算需要绘制的虚线数量
const lineCount = Math.floor(CHART_HEIGHT / AREA_HEIGHT);
for (let i = 0; i <= lineCount; i++) {
const y = CHART_HEIGHT - i * AREA_HEIGHT;
lines.push(
<Line
key={`reference-line-${i}`}
x1={MARGIN}
y1={y}
x2={SCREEN_WIDTH - MARGIN}
y2={y}
stroke="red"
strokeWidth="1"
strokeDasharray="4 4"
/>,
);
}
// 添加两条y轴边线
lines.push(
<Line
key={`reference-yline-0`}
x1={MARGIN}
y1={0}
x2={MARGIN}
y2={CHART_HEIGHT}
stroke="red"
strokeWidth="1"
strokeDasharray="4 4"
/>,
);
lines.push(
<Line
key={`reference-yline-1`}
x1={SCREEN_WIDTH - MARGIN}
y1={0}
x2={SCREEN_WIDTH - MARGIN}
y2={CHART_HEIGHT}
stroke="red"
strokeWidth="1"
strokeDasharray="4 4"
/>,
);
return lines;
};
构造一个对象来描述阶段颜色等配置信息,它和SleepStageEnum是一一对应关系
typescript
// 自定义形状配置字典
const STAGE_CONFIG = {
0: {
gradient: ['#8314f3', '#3800FF'], // 渐变色设置
useColorTransform: true, // 是否使用颜色变换
transformX1: '0%',
transformX2: '0%',
transformY1: '0%',
transformY2: '100%',
shadow: '#3800FF', // 阴影颜色
color: '#3800FF', // 默认颜色
label: '深睡眠', // 文字标签
},
1: {
gradient: ['#d248b0', '#ba2eec'],
useColorTransform: true,
transformX1: '0%',
transformX2: '0%',
transformY1: '0%',
transformY2: '100%',
color: '#ba2eec',
shadow: '#ba2eec',
label: '浅睡眠',
},
2: {
gradient: ['#e05891', '#f86d5a'],
useColorTransform: true,
transformX1: '0%',
transformX2: '0%',
transformY1: '0%',
transformY2: '100%',
shadow: '#f86d5a',
color: '#f86d5a',
label: '快速眼动',
},
3: {
gradient: ['#fb8b44', '#fcbb29'],
useColorTransform: true,
transformX1: '0%',
transformX2: '0%',
transformY1: '0%',
transformY2: '100%',
shadow: '#fcbb29',
color: '#fcbb29',
label: '清醒',
},
};
绘制一个辅助说明区域
typescript
<View style={styles.legend}>
{Object.entries(STAGE_CONFIG).map(([stage, config]) => (
<View key={stage} style={styles.legendItem}>
<View style={[styles.legendDot, {backgroundColor: config.color}]} />
<Text style={styles.legendText}>{config.label}</Text>
</View>
))}
</View>
在添加了辅助线与说明文案后,这时候的样式开始有点表格的味道了。

但它还只是点,下一步我们通过这些点来绘制出一些区域。
To Be Continued...
不经常在线,有问题可在微信公众号或者掘金社区私信留言
更多内容可关注
我的公众号悬空八只脚