0: 项目需求

近期项目有了新的需求, 需要根据地震数据绘制出对应图表, 关于这种图的资料比较少, 翻了不少网站, 也没找到太多有用的数据, 而关于Qt的, 更是只有一篇论文. 不过搜这么多资料也不是一无所获, 最起码知道了这种图的名字. 如标题所示, 下文统一称其为地震剖面图.

1: 图形分析:

上图是我查资料时找到的一张地震剖面图的图片, 可以看出,横轴代表通道, 纵轴代表时间, 图表中的折线按照则代表了震动的强度和方向(这一点说的可能不准确),  震动起来的部分,超出某个值的, 则将波峰染成黑色, 波谷则不做处理

2: Qt效果展示

在继续分析之前, 先看下用Qt实现的效果

3: 图形分解

如第一张图所示,   图中的折线, 坐标轴等, 可以用QCustomplot来实现. 关于波峰染色,  QCustomPlot在使用时, 如果设置了Brush笔刷, 那么其实就自带染色, 只不过效果可能与我们的需求有差异.

下面是一个简单的折线图, 然后加上笔刷 截图, 可以看到, 箭头指向的部分, 就是我们需要的染色效果. 但是这和我们的需求其实还是有些差距

 主要有2点

1)  这里的染色是以0为基础,  颜色值从0到波形的折线图中进行填充, 并不能控制 "振幅超出某个值以后, 进行填充的效果,  并且, 地震剖面图是有很多条曲线的, 不可能都以0为起点,必然要加上偏移量显示"

2) 图形是横着的, 地震剖面图一般都是竖着, 所以这一点也需要做点调整

4: 首先尝试现有方案

QCustomPlot中, QGraph类有一个setChannelFillGraph函数, 可以将2个图层之间的部分进行填充, 下边进行测试

1) 先创建2个图层, 并设置数据看看

    QVector<double> x, y, y2;for(int i=0; i<11; i++){x.push_back(i);if(i%3 == 0 || i%4 == 0 || i%8 == 0){y.push_back(25);}else{y.push_back(10);}y2.push_back(20);}//设置交互属性, 可拖动,可缩放customPlot->setInteractions(QCP::iRangeDrag | QCP::iRangeZoom);//添加图层, 并设置画笔和笔刷颜色customPlot->addGraph();customPlot->graph(0)->setPen(QPen(Qt::black));customPlot->graph(0)->setBrush(QColor(255, 0, 0, 50));//设置数据进去customPlot->graph(0)->setData(x, y);//添加图层2, 并设置画笔和笔刷颜色customPlot->addGraph();customPlot->graph(1)->setPen(QPen(Qt::red));customPlot->graph(1)->setBrush(QColor(0, 0, 255, 50));//设置数据进去customPlot->graph(1)->setData(x, y2);//图层间填充customPlot->graph(0)->setChannelFillGraph(customPlot->graph(1));//设置坐标轴范围customPlot->xAxis->setRange(0, 10);customPlot->yAxis->setRange(-10, 30);//重绘customPlot->replot();

运行起来, 结果是这样的

如图所示, 比起直接设置默认笔刷, 图层间填充确实可以实现以某条线为分界, 然后以该线为中心, 进行填充( 或许这样看和直接设置笔刷没啥区别, 但是入如果把红线的笔刷透明, 或者直接把设置图层1笔刷的代码注释掉的话, 就能看出来区别了 )

//    customPlot->graph(1)->setBrush(QColor(0, 0, 255, 50));

那么,  问题来了, 是不是按照这个思路下去, 把红线下边的填充去掉, 只把红线上方的部分进行填充. 就完成任务了?

我认为是不行的,  使用这种方式, 显示一条曲线, 需要2个图层, 并且数据量也是要翻倍的. 如果只显示一条线还好说, 显示的线条数量很多的话, 必然会导致卡顿.

5: 源码分析

现有代码无法满足需求, 那么就只能对QCustomPlot进行二次开发了. 想要解决这个问题的第一步, 就是要找到, 笔刷填充部分的绘制逻辑

进入到QCustomplot.cpp源码中, 查找QCPGraph的 draw函数

void QCPGraph::draw(QCPPainter *painter)
{if (!mKeyAxis || !mValueAxis) { qDebug() << Q_FUNC_INFO << "invalid key or value axis"; return; }if (mKeyAxis.data()->range().size() <= 0 || mDataContainer->isEmpty()) return;if (mLineStyle == lsNone && mScatterStyle.isNone()) return;QVector<QPointF> lines, scatters; // line and (if necessary) scatter pixel coordinates will be stored here while iterating over segments// loop over and draw segments of unselected/selected data:QList<QCPDataRange> selectedSegments, unselectedSegments, allSegments;getDataSegments(selectedSegments, unselectedSegments);allSegments << unselectedSegments << selectedSegments;for (int i=0; i<allSegments.size(); ++i){bool isSelectedSegment = i >= unselectedSegments.size();// get line pixel points appropriate to line style:QCPDataRange lineDataRange = isSelectedSegment ? allSegments.at(i) : allSegments.at(i).adjusted(-1, 1); // unselected segments extend lines to bordering selected data point (safe to exceed total data bounds in first/last segment, getLines takes care)getLines(&lines, lineDataRange);// check data validity if flag set:
#ifdef QCUSTOMPLOT_CHECK_DATAQCPGraphDataContainer::const_iterator it;for (it = mDataContainer->constBegin(); it != mDataContainer->constEnd(); ++it){if (QCP::isInvalidData(it->key, it->value))qDebug() << Q_FUNC_INFO << "Data point at" << it->key << "invalid." << "Plottable name:" << name();}
#endif//注释的还是比较清除的, 在这里进行了图形填充的绘制// draw fill of graph:if (isSelectedSegment && mSelectionDecorator)mSelectionDecorator->applyBrush(painter);elsepainter->setBrush(mBrush);painter->setPen(Qt::NoPen);drawFill(painter, &lines);// draw line:if (mLineStyle != lsNone){if (isSelectedSegment && mSelectionDecorator)mSelectionDecorator->applyPen(painter);elsepainter->setPen(mPen);painter->setBrush(Qt::NoBrush);if (mLineStyle == lsImpulse)drawImpulsePlot(painter, lines);elsedrawLinePlot(painter, lines); // also step plots can be drawn as a line plot}// draw scatters:QCPScatterStyle finalScatterStyle = mScatterStyle;if (isSelectedSegment && mSelectionDecorator)finalScatterStyle = mSelectionDecorator->getFinalScatterStyle(mScatterStyle);if (!finalScatterStyle.isNone()){getScatters(&scatters, allSegments.at(i));drawScatterPlot(painter, scatters, finalScatterStyle);}}// draw other selection decoration that isn't just line/scatter pens and brushes:if (mSelectionDecorator)mSelectionDecorator->drawDecoration(painter, selection());
}

上边的代码段是QCPGraph的draw函数内容, 如注释所示, 绘制填充的地方, 在drawFill函数中

那么我们进入到drawFill函数中看一下

void QCPGraph::drawFill(QCPPainter *painter, QVector<QPointF> *lines) const
{if (mLineStyle == lsImpulse) return; // fill doesn't make sense for impulse plotif (painter->brush().style() == Qt::NoBrush || painter->brush().color().alpha() == 0) return;applyFillAntialiasingHint(painter);const QVector<QCPDataRange> segments = getNonNanSegments(lines, keyAxis()->orientation());//如果没有设置与目标图层绘图的话, 就绘制一个从曲线到坐标轴的0位置的闭合图形if (!mChannelFillGraph){// draw base fill under graph, fill goes all the way to the zero-value-line:foreach (QCPDataRange segment, segments)painter->drawPolygon(getFillPolygon(lines, segment));}else         //如果设置了目标图层填充, 那就绘制从当前图层到目标图层填充的闭合图形{// draw fill between this graph and mChannelFillGraph:QVector<QPointF> otherLines;mChannelFillGraph->getLines(&otherLines, QCPDataRange(0, mChannelFillGraph->dataCount()));if (!otherLines.isEmpty()){QVector<QCPDataRange> otherSegments = getNonNanSegments(&otherLines, mChannelFillGraph->keyAxis()->orientation());QVector<QPair<QCPDataRange, QCPDataRange> > segmentPairs = getOverlappingSegments(segments, lines, otherSegments, &otherLines);for (int i=0; i<segmentPairs.size(); ++i)painter->drawPolygon(getChannelFillPolygon(lines, segmentPairs.at(i).first, &otherLines, segmentPairs.at(i).second));}}
}

上边的代码段我加了注释

mChannelFillGraph 变量是从哪来的, 我们可以看一下

void QCPGraph::setChannelFillGraph(QCPGraph *targetGraph)
{// prevent setting channel target to this graph itself:if (targetGraph == this){qDebug() << Q_FUNC_INFO << "targetGraph is this graph itself";mChannelFillGraph = nullptr;return;}// prevent setting channel target to a graph not in the plot:if (targetGraph && targetGraph->mParentPlot != mParentPlot){qDebug() << Q_FUNC_INFO << "targetGraph not in same plot";mChannelFillGraph = nullptr;return;}/* 没错, 我们之前测试的时候, 进行图层间填充, 调用了 setChannelFillGraph 函数, 而这个函数最终会修改 mChannelFillGraph 的值*/ mChannelFillGraph = targetGraph;
}

如图所示, 如果我们调用了图层间填充的设置函数,  drawFill函数, 就会进入到 else 选项中, 否则就是绘制从曲线到坐标轴0线的填充, painter->drawPolygon(), 这个函数是QPainter对象的绘制多边形的函数, 在这里我们暂且跳过, 我们需要关注的 是 getFillPolygon, 从名字上可以看出, 这个函数返回一个将要被填充的多边形范围

那么我们进入到 getFillPolygon 函数看看

//此函数返回一个形状, 依靠这个形状进行绘图
const QPolygonF QCPGraph::getFillPolygon(const QVector<QPointF> *lineData, QCPDataRange segment) const
{if (segment.size() < 2)return QPolygonF();QPolygonF result(segment.size()+2);result[0] = getFillBasePoint(lineData->at(segment.begin()));std::copy(lineData->constBegin()+segment.begin(), lineData->constBegin()+segment.end(), result.begin()+1);result.push_back(getFillBasePoint(lineData->at(segment.end()-1)));result[result.size()-1] = getFillBasePoint(lineData->at(segment.end()-1));return result;
}

getFillPolygon 函数, 通过传递进来的折线图的顶点列表, 再结合 getFillBasePoint 函数获取基线坐标(基线坐标指的是 X轴为水平坐标轴的情况下,  坐标轴 Y轴为0, X轴最左边和X轴最右边, 获取到的2个坐标). 这也就解释了为什么图形填充为什么总是填充到0

那么我们需要动的地方, 就在这里了, 有折线图的顶点坐标列表, 我们就能根据自己的需求, 实现一个我们想要的填充多边形

为了验证我们的猜想, 这里先进行一下测试, 直接返回一个固定形状, 看下是否会绘制出来

注意, 需要把 之前设置的图层间填充的代码屏蔽掉

// customPlot->graph(0)->setChannelFillGraph(customPlot->graph(1));

修改getFillPolygon函数如下

//此函数返回一个形状, 依靠这个形状进行绘图
const QPolygonF QCPGraph::getFillPolygon(const QVector<QPointF> *lineData, QCPDataRange segment) const
{//        if (segment.size() < 2)
//            return QPolygonF();
//        QPolygonF result(segment.size()+2);
//        result[0] = getFillBasePoint(lineData->at(segment.begin()));//        std::copy(lineData->constBegin()+segment.begin(), lineData->constBegin()+segment.end(), result.begin()+1);//        result.push_back(getFillBasePoint(lineData->at(segment.end()-1)));
//        result[result.size()-1] = getFillBasePoint(lineData->at(segment.end()-1));
//        return result;//这里我们直接返回一个三角形QPolygonF result;result.append(QPointF(150, 50));result.append(QPointF(50, 150));result.append(QPointF(250, 150));return result;}

然后编译运行, 结果如下所示

可以看到, 和我们预想的一致, 确实绘制出来了一个三角形.

既然这样, 那我们完全可以按照顶点数据, 重新组组装出来一个多边形结构, 让它将超出我们设置的阈值的数据部分进行填充, 低于该值的则不填充.

理论可行, 接下来进行修改

再次回到源码部分,

//此函数返回一个形状, 依靠这个形状进行绘图
const QPolygonF QCPGraph::getFillPolygon(const QVector<QPointF> *lineData, QCPDataRange segment) const
{if (segment.size() < 2)return QPolygonF();QPolygonF result(segment.size()+2);result[0] = getFillBasePoint(lineData->at(segment.begin()));//这里把顶点坐标复制到了多边形顶点坐标中, 这里我们做点处理std::copy(lineData->constBegin()+segment.begin(), lineData->constBegin()+segment.end(), result.begin()+1);result.push_back(getFillBasePoint(lineData->at(segment.end()-1)));result[result.size()-1] = getFillBasePoint(lineData->at(segment.end()-1));return result;
}

代码中的std::copy那一句, 把折线图的顶点坐标复制到了result容器中, result容器,正是保存填充色的容器, 我们就在复制这一步, 做点东西.

既然是超出阈值的才进行染色, 那么不难想到, 我们把数据低于阈值的, 全部设置成和阈值相等,  然后把基线也提升到和阈值相等,  是不是就行了?

事实上,  如果只这么操作的话,  是会有点问题的, 先看下效果

//此函数返回一个形状, 依靠这个形状进行绘图
const QPolygonF QCPGraph::getFillPolygon(const QVector<QPointF> *lineData, QCPDataRange segment) const
{if (segment.size() < 2)return QPolygonF();QPolygonF result(segment.size()+2);
//        result[0] = getFillBasePoint(lineData->at(segment.begin()));//        std::copy(lineData->constBegin()+segment.begin(), lineData->constBegin()+segment.end(), result.begin()+1);//计算出染色基准线, 这里把阈值设置为20, 也就是说, Y轴数据超出20的部分进行染色double divding = 20;QPointF start((*lineData)[0].x(), mValueAxis->coordToPixel(divding));QPointF end((*lineData).last().x(), mValueAxis->coordToPixel(divding));//基线起点result[0] = start;double divdingPix = mValueAxis->coordToPixel(divding);for(int i=0; i<lineData->size(); i++){if((*lineData)[i].y() <= divdingPix)            //注意, lineData里边保存的是折线图的图像坐标, 左上角是0,0, 而不是我们图形中的左下角是 0,0{result.push_back((*lineData)[i]);}else{//所有低于阈值的, 都设置成阈值result.push_back(QPointF((*lineData)[i].x(), divdingPix));}}//基线终点result[result.size()-1] = end;//        result.push_back(getFillBasePoint(lineData->at(segment.end()-1)));
//        result[result.size()-1] = getFillBasePoint(lineData->at(segment.end()-1));return result;
}

将基线位置, 和多边形生成逻辑进行修改, 最终效果如下图

可以看到, 低于阈值的数据确实没了,  但是图好像塌下来了, 其实这个也好理解, 我们把低于阈值的数据全部设置的和阈值相等,  所以只有Y轴被提上去了,  但是折线从阈值线上跨切过去的点并没有被添加到多边形顶点坐标中

所以在这之前, 还需要有一步, 就是把X轴的位置找出来, 也就是从阈值线上跨切过去的坐标

分析出来了原因, 那就继续往下走,  把这些跨切坐标找到, 并添加到多边形顶点坐标中

修改之后的代码如下所示

//此函数返回一个形状, 依靠这个形状进行绘图
const QPolygonF QCPGraph::getFillPolygon(const QVector<QPointF> *lineData, QCPDataRange segment) const
{//闭合区间第一个点: QPointF(40.2451,542)//使用交点检测方式插入数据if (segment.size() < 1)return QPolygonF();QPolygonF result(1);//计算出染色基准线double  divding = 20;     QPointF start((*lineData)[0].x(), mValueAxis->coordToPixel(divding));QPointF end((*lineData).last().x(), mValueAxis->coordToPixel(divding));//闭合区间第一个点result[0] = QPointF(start.x(), mValueAxis->coordToPixel(divding));//阈值线QLineF line1(start, end);QPointF po;int pointCount = 0;//生成一个带有跨切点的顶点坐标列表QVector<QPointF> out;for(int i=0; i<lineData->size()-1; i++){//使用QPointF类的判断线段交点的函数寻找交点QLineF line2((*lineData)[i], (*lineData)[i+1]);out.push_back((*lineData)[i]);if(line2.intersects(line1, &po)){out.push_back(po);++pointCount;}}out.push_back(lineData->last());          //把缺失的最后一个点补上//这里遍历带有跨切点的像素坐标列表, 同时对数据大于限定值的数据进行限制for(int i=0; i<out.size(); i++){//这里比较的是像素, 因此坐标轴上小的值,在这里反而会比较大if(out[i].y() > start.y()){//如果数据值小于阈值线, 那就设置其和阈值线相等result.push_back(QPointF(out[i].x(), start.y()));}else{result.push_back(out[i]);}}//打印一下顶点数量和交点数量qDebug() << u8"总的点数:" << result.size() << u8"交点数量:" << pointCount;//闭合区间最后一个点result.push_back(end);return result;
}

上边的代码片中, 使用QPointF类的 intersects 函数, 找到了线段的交点, 然后将这个交点也加入到临时顶点坐标中. 然后对临时顶点坐标进行遍历, 并将小于阈值的数据修正到和阈值相等.

进行完了这一步之后,  输出就比较接近我们的需求了

调试输出打印:

总的点数: 19 交点数量: 7

不考虑调试输出的话, 效果似乎还不错, 但是, 调试输出显示, 顶点数量有19个, 但我们的折线图中, 其实是没这么多顶点的,  而导致顶点数增加的原因, 就在于小于阈值的点, 我们把它移动到了阈值的Y轴位置,  但这一步其实可以省略. 因为有了最左侧和最右侧的蓝色圈位置的顶点, 就已经可以决定填充色多边形的走向了. 因此, 可以把上边代码中的小于阈值部分, 移动到阈值部分的代码注释掉, 直接丢弃这个顶点坐标

//这里比较的是像素, 因此坐标轴上小的值,在这里反而会比较大
if(out[i].y() > start.y())
{//数据值小于阈值线, 那就完全可以丢弃了, 这里就不往顶点数据里边加了, 直接注释掉//result.push_back(QPointF(out[i].x(), start.y()));
}
else
{result.push_back(out[i]);
}

再次运行一下代码,  显示的结果一样, 但是顶点数量就少了

调试输出打印

总的点数: 14 交点数量: 7

可以看到, 相比原来的19个顶点, 现在变成了14个顶点,  这少掉的5个顶点, 其实就是小于阈值的点

这5个顶点, 从填充多边形的顶点中删除掉了, 但是由于跨切交点的存在, 所以对图形展现并没有影响, 等到数据量多起来的时候, 这一操作可以有效的省略掉大量的点.

比如我们把点数增加到100看看

总的点数: 168 交点数量: 66
总的点数: 118 交点数量: 66

仅仅100个数据点的情况下, 就少掉了50个点, 如果每一条线的点数达到几十K的时候, 这些点数对速度和内存的影响就会大起来.

写到这里, 后边其实也就没什么好说的了

把图形竖起来的话,  只需要把X轴设置成Y轴, Y轴设置成X轴就行了

customPlot->graph(0)->setKeyAxis(ui->customPlot->yAxis);
customPlot->graph(0)->setValueAxis(ui->customPlot->xAxis);

然后再加个阈值变量, 基本上就完事了

6: 效果展示

最后, 在前边的代码基础上, 添加几个图层, 加一些数据看下效果

改个颜色, 就得到了标题图

Qt 地震剖面图(或者叫地震摆动图,波形变面积图)相关推荐

  1. Matlab画地球剖面图,分享用matlab显示地震记录的波形变面积图

    wigb函数是网上找的,自己添了个主程序方便使用. 主程序 function PlotRecord_Wigb(filename,nx,nt,dx,dt,zy) %波形变面积显示地震记录 % clear ...

  2. Python使用matplotlib可视化面积图(Area Chart)、通过给坐标轴和曲线之间的区域着色可视化面积图、在面积图的指定区域添加箭头和数值标签

    Python使用matplotlib可视化面积图(Area Chart).通过给坐标轴和曲线之间的区域着色可视化面积图.在面积图的指定区域添加箭头和数值标签 目录

  3. Python使用matplotlib可视化时间序列堆叠的面积图、堆叠面积图给出了多个时间序列的贡献程度的可视化表示,以便于相互比较(Stacked Area Chart)

    Python使用matplotlib可视化时间序列堆叠的面积图.堆叠面积图给出了多个时间序列的贡献程度的可视化表示,以便于相互比较(Stacked Area Chart) 目录

  4. 用Python pyecharts v1.x 绘制图形(二):折线图、折线面积图、散点图、雷达图、箱线图、词云图

    文章目录 关于pyecharts 折线图 折线面积图 散点图 雷达图 箱线图 词云图 其他 关于pyecharts pyecharts是一个用于生成echart(百度开源的数据可视化javascrip ...

  5. 峰谷图配置(面积图, 基于echarts)

    峰谷图或面积图也是数据可视化图表的一种常见类型.这个配置比较简单,下面就以一个销量峰谷图为例直接放效果图和代码了.下面是最终实现的效果动态图,提示框会跟随鼠标移动而确定位置: 为了有更好的适用性,这里 ...

  6. 复现Nature图表 ggplot做面积图(折线面积图)

    今天我们学做一下nature文章(Deciphering human macrophage development at single-cell resolution)的图表,本意是展示细胞比例的变化 ...

  7. R语言数据可视化之折线图、堆积图、堆积面积图

    折线图简介 折线图通常用来对两个连续变量的依存关系进行可视化,其中横轴很多时候是时间轴. 但横轴也不一定是连续型变量,可以是有序的离散型变量. 绘制基本折线图 本例选用如下测试数据集: 绘制方法是首先 ...

  8. Echarts折线图之区域面积图

    今天写echarts发现有个比较坑的地方,我使用了areaStyle 这个属性,把折线图设置成了如图的样式 但是客户想要的是如下图在这个样式 这里要敲重点了,想实现这个样式必须使用series-> ...

  9. Qt编写可视化大屏电子看板系统15-曲线面积图

    一.前言 曲线面积图其实就是在曲线图上增加了颜色填充,单纯的曲线可能就只有线条以及数据点,面积图则需要从坐标轴的左下角和右下角联合曲线形成完整的封闭区域路径,然后对这个路径进行颜色填充,为了更美观的效 ...

最新文章

  1. Patreon禁用加密货币?不好意思,基于BCH的Bitreon即将上线
  2. 华为手机接计算机,华为手机怎么连接电脑,详细教您华为手机怎么连接电脑
  3. android怎么打开wifi的组播功能
  4. c语言最长公共子序列_序列比对(二十四)——最长公共子序列
  5. 工业用计算机使用环境温度范围,IEC 61000-2-2
  6. 团队作业2之选题与评审
  7. 《Flutter 从0到1构建大前端应用》读后感—第6章【使用网络技术与异步编程】
  8. 【JAVA】Collections.sort()实现动态数组自定义排序
  9. python代码缩进中是否支持tab键和空格混用_python自测——编码规范
  10. html 预加载图片,实现网页图片预加载的几个方法
  11. 计算机网络TETP功能和作用,常见tftp命令及用法
  12. angular里的$even和$odd的应用
  13. spss三次指数平滑_15.2.2 指数平滑模型的SPSS操作(1)
  14. 手机控制电脑远程开机,笔记本与老电脑都能实现
  15. TP-LINK wn822n USB型无线网卡win10环境下驱动程序
  16. 1014: 统计患病人数
  17. 微信退款证书使用c#
  18. 机器学习与计算机视觉入门项目——视频投篮检测(三)
  19. 我在江北学安全(五) 渗透测试资源总览 和 XSS扫描系统原理 (续)
  20. JAVA 赶鸭子问题 递归求解

热门文章

  1. SVN 提示 Failed to run the WC DB work queue 错误解决
  2. 卸载oracle指定在此,卸载本地oracle(完整版)
  3. 野火学习笔记(4) —— 固件库
  4. wordpress批量发布文章 python_python自动化测试之wordpress发布文章
  5. xilinx zynq RapidIO系统配置
  6. 算法-----劳斯-赫尔维茨(Routh-Hurwitz)稳定判据(转)
  7. 国内数据库技术大牛:牛新庄博士自传(转)
  8. 数据结构 DAY05 栈的应用之中缀表达式转换
  9. c语言苏小江第九章实验题答案,蓝桥杯C语言C组校内赛题目解析
  10. oracle定时备份SHELL,shell数据库备份脚本oracle