高级图表实现解决方案
方案描述
mpchart是一个包含各种类型图表的图表库,主要用于业务数据汇总,例如销售数据走势图,股价走势图等场景中使用,方便开发者快速实现图表UI,mpchart主要包括线形图、柱状图、饼状图、蜡烛图、气泡图、雷达图、瀑布图等自定义图表库。
使用准备
1. 下载三方库控制台输入:ohpm install @ohos/mpchart。
2. 初始化图表配置构建类。
初始化三方库得构建类,图表数据,线性数据等。
import { LineChart, LineChartModel } from '@ohos/mpchart'; // 初始化图表配置构建类
import { XAxis, XAxisPosition } from '@ohos/mpchart'; // x轴
import { YAxis, AxisDependency, YAxisLabelPosition } from '@ohos/mpchart'; // Y轴
import { LineData } from '@ohos/mpchart'; // 生成图表数据
import { LineDataSet, Mode } from '@ohos/mpchart'; //线形图数据集合
import { EntryOhos } from '@ohos/mpchart'; //图表数据结构基础类
3. 配置图表指定样式。
注:此步骤是初始化底层的表框架,也是就后面的网格。
图表生成分为x、y轴。
setPosition设置:
x轴(getXAxis)得显示范围:TOP, BOTTOM, BOTH_SIDED, TOP_INSIDE, BOTTOM_INSIDE,x轴只需要设置一次即可。
y轴(getAxisLeft,getAxisRight)表内左右显示范围:OUTSIDE_CHART, INSIDE_CHART,如需只设置一侧,创建实例时,添加属性添加所需侧即可。
let topAxis = this.model.getXAxis();//图表x轴
if (topAxis) {
topAxis.setLabelCount(5, false);//设置绘制标签个数
topAxis.setPosition(XAxisPosition.TOP);//设置标签位置
topAxis.setAxisMinimum(0); //设置最小值
topAxis.setAxisMaximum(50); //设置最大值
topAxis.setDrawAxisLine(false)// 启用绘制x轴轴线
topAxis.setDrawLabels(false) // 不绘制x轴标签
topAxis.setDrawGridLines(false)// 不绘制X轴网格线
}
let leftAxis = this.model.getAxisLeft();
if (leftAxis) {
leftAxis = new YAxis(AxisDependency.LEFT);
leftAxis.setLabelCount(6, false);
leftAxis.setDrawGridLines(true);//启用绘制左侧Y轴网格线
leftAxis.setPosition(YAxisLabelPosition.OUTSIDE_CHART);
leftAxis.setAxisMinimum(0);
leftAxis.setAxisMaximum(50);
leftAxis.setDrawLabels(true);
leftAxis.setDrawGridLines(true);
}
let rightAxis = this.model.getAxisRight();
if (rightAxis) {
rightAxis = new YAxis(AxisDependency.RIGHT);
rightAxis.setDrawGridLines(true);
rightAxis.setLabelCount(6, false);
rightAxis.setAxisMinimum(0);
rightAxis.setAxisMaximum(50);
rightAxis.setDrawAxisLine(false);
rightAxis.setDrawLabels(true);
rightAxis.setDrawGridLines(true);
}
this.model.setData(this.lineData);
应用场景
解决方案:mpchart
场景一:线性表虚实线交接【曲线】
实现效果
图1-1
核心代码
我们需要写一个MyLineDataSet类,继承自LineDataSet,也就是线型图的数据类。为什么需要这个类呢?因为我们需要在初始化数据的时候定义这个虚实相接的线是从哪里开始由实线变为虚线的,这里MyLineDataSet类的构造方法比它的父类多了一个interval参数,也就是虚实分隔点。
import { EntryOhos, JArrayList, LineDataSet } from '@ohos/mpchart';
export class MyLineDataSet extends LineDataSet {
interval: number = 0;
constructor(yVals: JArrayList<EntryOhos> | null, label: string, interval: number) {
super(yVals, label);
this.interval = interval;
}
}
定义好自己的数据类之后,就要定义MyRender类了,实线具体的绘制功能,MyRender类继承自LineChartRenderer,因为是要绘制曲线,所以重写的是drawCubicBezier方法,MyRender类的代码。
import { EntryOhos, JArrayList, LineDataSet } from '@ohos/mpchart';
export default class MyRender extends LineChartRenderer {
protected drawCubicBezier(c: CanvasRenderingContext2D, dataSet: MyLineDataSet) {
if (!this.mChart || !this.mXBounds) {
return;
}
const PHASE_Y: number = this.mAnimator ? this.mAnimator.getPhaseY() : 1;
const TRANS: Transformer | null = this.mChart.getTransformer(dataSet.getAxisDependency());
this.mXBounds.set(this.mChart, dataSet);
const intensity: number = dataSet.getCubicIntensity();
let lineCubicPath = new Path2D();
let dotsCubicPath = new Path2D();
if (this.mXBounds.range >= 1) {
let prevDx: number = 0;
let prevDy: number = 0;
let curDx: number = 0;
let curDy: number = 0;
const FIRST_INDEX: number = this.mXBounds.min + 1;
const INTERVAL: number = dataSet.interval;
let prevPrev: EntryOhos | null;
let prev: EntryOhos | null = dataSet.getEntryForIndex(Math.max(FIRST_INDEX - 2, 0));
let cur: EntryOhos | null = dataSet.getEntryForIndex(Math.max(FIRST_INDEX - 1, 0));
let next: EntryOhos | null = cur;
let nextIndex: number = -1;
if (cur === null) return;
Utils.resetContext2DWithoutFont(c, this.mRenderPaint);
// let the spline start
lineCubicPath.moveTo(cur.getX(), cur.getY() * PHASE_Y);
for (let j: number = this.mXBounds.min + 1; j <= this.mXBounds.range + this.mXBounds.min; j++) {
prevPrev = prev;
prev = cur;
cur = nextIndex === j ? next : dataSet.getEntryForIndex(j);
nextIndex = j + 1 < dataSet.getEntryCount() ? j + 1 : j;
next = dataSet.getEntryForIndex(nextIndex);
prevDx = (cur.getX() - prevPrev.getX()) * intensity;
prevDy = (cur.getY() - prevPrev.getY()) * intensity;
curDx = (next.getX() - prev.getX()) * intensity;
curDy = (next.getY() - prev.getY()) * intensity;
if (j < INTERVAL) {
lineCubicPath.bezierCurveTo(
prev.getX() + prevDx,
(prev.getY() + prevDy) * PHASE_Y,
cur.getX() - curDx,
(cur.getY() - curDy) * PHASE_Y,
cur.getX(),
cur.getY() * PHASE_Y
);
if (j === INTERVAL - 1) {
dotsCubicPath.moveTo(cur.getX(), cur.getY());
}
} else {
dotsCubicPath.bezierCurveTo(
prev.getX() + prevDx,
(prev.getY() + prevDy) * PHASE_Y,
cur.getX() - curDx,
(cur.getY() - curDy) * PHASE_Y,
cur.getX(),
cur.getY() * PHASE_Y
);
}
}
}
// if filled is enabled, close the path
if (dataSet.isDrawFilledEnabled()) {
let cubicFillPath: Path2D = new Path2D();
cubicFillPath.addPath(lineCubicPath);
if (c && TRANS) {
this.drawCubicFill(c, dataSet, cubicFillPath, TRANS, this.mXBounds);
}
}
this.mRenderPaint.setColor(dataSet.getColor());
this.mRenderPaint.setStyle(Style.STROKE);
if (TRANS && TRANS.pathValueToPixel(lineCubicPath)) {
lineCubicPath = TRANS.pathValueToPixel(lineCubicPath);
}
if (TRANS && TRANS.pathValueToPixel(dotsCubicPath)) {
dotsCubicPath = TRANS.pathValueToPixel(dotsCubicPath);
}
Utils.resetContext2DWithoutFont(c, this.mRenderPaint);
c.beginPath();
c.stroke(lineCubicPath);
c.closePath();
Utils.resetContext2DWithoutFont(c, this.mRenderPaint);
c.beginPath();
c.setLineDash([5, 5, 0])
c.stroke(dotsCubicPath);
c.closePath();
this.mRenderPaint.setDashPathEffect(null);
}
}
这个方法主要内容就是定义了两条path2D,也就是线段来绘制实线和虚线。
//实线
let solidLinePath = new Path2D();
//虚线
let dashedLinePath = new Path2D();
绘制方法如下:
solidLinePath.bezierCurveTo(
prev.getX() + prevDx,
(prev.getY() + prevDy) * phaseY,
cur.getX() - curDx,
(cur.getY() - curDy) * phaseY,
cur.getX(),
cur.getY() * phaseY
);
就是调用path2D的方法bezierCurveTo方法,这个方法有6个参数,分别是控制点1的(x值,y值 )和 控制点2的(x值,y值)以及目标点的(x值,y值)。直接把父类的方法抄过来即可。
我们需要有一个if判断,if(j <= dataSet.interval)就是当j小于dataSet.interval时,写绘制实线的方法,当j等于dataSet.interval时,虚线要moveTo当前位置;当j大于dataSet.interval时,就调用dashedLinePath.bezierCurveTo方法绘制虚线了。
最后绘制方法是调用c.stroke方法绘制的。c.setLineDash([5,5,0]);是设置虚线效果。
Utils.resetContext2DWithoutFont(c, this.mRenderPaint);
c.beginPath();
c.stroke(solidLinePath);
c.closePath();
Utils.resetContext2DWithoutFont(c, this.mRenderPaint);
c.beginPath();
c.setLineDash([5,5,0]);
c.stroke(dashedLinePath);
c.closePath();
最后就是使用,代码如下:
// 创建一个 JArrayList 对象,用于存储 EntryOhos 类型的数据
let values: JArrayList<EntryOhos> = new JArrayList<EntryOhos>();
// 循环生成 1 到 6 的随机数据,并添加到 values 中
//i为图表内数据线的节点数
for (let i = 1; i <= 6; i++) {
values.add(new EntryOhos(i, Math.random() * 100));
}
// 创建 LineDataSet 对象,使用 values 数据,并设置数据集的名称为 'DataSet'
//3是图表内数据线实线的数量(按节点来算,如果2个节点就为一条线性数据)
let dataSet = new MyLineDataSet(values, 'DataSet', 3);
//设置线条模式
dataSet.setMode(Mode.CUBIC_BEZIER);
let dataSetList: JArrayList<ILineDataSet> = new JArrayList<ILineDataSet>();
dataSetList.add(dataSet);
// 创建 LineData 对象,使用 dataSetList数据,并将其传递给model
let lineData: LineData = new LineData(dataSetList);
this.model?.setData(lineData);
//canvas绘制
this.model.setRenderer(new MyRender(this.model, this.model.getAnimator()!, this.model.getViewPortHandler()))、
//动画播放时长
this.model.animateX(5000);
场景二:线性表模拟数据图表实时变化
实现效果
- 绘制图表:添加底层图表数据。
- 绘制线标:添加实时线性数据。
- 清空:清空线性数据。
核心代码
初始化底层表格后,需要给表格上绑定数据。
首先判定data是否有数据,不为空的话获取这个DataSet这个容器,如果容器DataSet为空,添加样式即可。
调用接口addEntry填充数据,将之前的生成的数据用set.getEntryCount一起添加,再利用随机数填充新数据,从0索引开始。
notifyDataChanged计算数据的最大值和最小值,notifyDataSetChanged触发数据更新,以x轴移动。
注:每次通过调用addEntry()获取set容器不断更新里面的数据做渲染。
addEntry() {
let data = this.model.getData();//获取数据
if (data != null) {
let set = data.getDataSetByIndex(0);
if (set == null) {
set = this.createSet();//填充样式
data.addDataSet(set);
}
data.addEntry(new EntryOhos(set.getEntryCount(), (Math.random() * 40) + 30), 0);//填充数据
data.notifyDataChanged();
this.model.notifyDataSetChanged();//触发坐标轴数据更新
this.model.setVisibleXRangeMaximum(50);// 设置图表最大的X轴显示范围,如不设置,则默认显示全部数据
this.model.moveViewToX(data.getEntryCount());//以X轴移动
}
}
场景三:线性表自定义表节点和线性节点
实现效果
上方slider:x轴传值监听,下方slider:y轴传值监听。
注册监听事件"seekBarXValueWatch"和"seekBarYValueWatch",滑动组件Slider,当值变化时触发监听,传递给setData。
核心代码
@State @Watch("seekBarXValueWatch") seekBarX: SeekBarModel = new SeekBarModel()//x轴传值监听
@State @Watch("seekBarYValueWatch") seekBarY: SeekBarModel = new SeekBarModel()//Y轴传值监听
seekBarXValueWatch(): void {
this.setData(this.seekBarX.getValue(), this.seekBarY.getValue());
}
seekBarYValueWatch(): void {
this.setData(this.seekBarX.getValue(), this.seekBarY.getValue());
}
将监听后数据传入setData,经过计算得出X,Y轴的数量,传入数据结构基础类JArrayList<EntryOhos>,添加样式,绑定图表。
注:每次滑动slider改变的时候,触发监听将值传递给setData重新计算数据,达到变化效果。
private async setData(count: number, range: number): Promise<void> {
let start: number = 1;
let values: JArrayList<EntryOhos> = new JArrayList<EntryOhos>();
for (let i = start; i < count; i++) { //X轴坐标个数
// 设置数据点Y值
let val = Math.random() * range; //Y轴值
values.add(new EntryOhos(i, val));
}
//添加样式
this.dataSet = new LineDataSet(values, "DataSet 1");
this.dataSet.setDrawIcons(false)
this.dataSet.setDrawFilled(true);//是否显示阴影
this.dataSet.setDrawValues(true);
this.dataSet.setMode(Mode.LINEAR);//直线模式
this.dataSet.setColorByColor(ChartColor.rgb(140, 234, 255));//设置折线颜色
this.dataSet.setLineWidth(1.5)
this.dataSet.setDrawCircles(true);//折线点画圆圈
this.dataSet.setDrawIcons(false) // 设置是否绘制数据项图标
this.dataSet.setCircleColor(ChartColor.rgb(140, 234, 255));//设置圆环颜色
// 设置数据点的半径
this.dataSet.setCircleRadius(4);//设置内径
this.dataSet.setCircleHoleRadius(2)// 设置半径大小
this.dataSet.setCircleHoleColor(Color.White)
this.dataSet.setDrawCircleHole(true)//设置内部孔
let gradientFillColor = new JArrayList<ChartColorStop>();//渐变颜色类 设置一种颜色即可折现内部
gradientFillColor.add(["#FFB6C1", 0.2]);
gradientFillColor.add(["#DA70D6", 0.4]);
gradientFillColor.add(["#0000FF", 0.6]);
gradientFillColor.add(["#FFD700", 0.8]);
gradientFillColor.add(["#90EE90", 1.0]);
this.dataSet.setGradientFillColor(gradientFillColor);//塞入渐变数据
this.dataSet.setDrawFilled(true);//填充绘制
let dataSetList: JArrayList<ILineDataSet> = new JArrayList<ILineDataSet>();
dataSetList.add(this.dataSet);//// 线形图数据集合的操作类添加dataset设置好的数据
let lineData: LineData = new LineData(dataSetList);//生成图表数据
lineData.setValueTextSize(10);//设置文本大小
if (this.model) {
this.model.setData(lineData);// 将数据与图表配置类绑定
}
}
注意事项
aboutToAppear定义x/y轴的外层图表时,x轴只用定义一次,Y轴根据业务来。
设置渐变颜色线性内的范围不是所有渐变,而只是最表层,如需设置渐变,只推荐两种相邻颜色过度。