Preparations for limit lines on XY-plot

This commit is contained in:
Jan Käberich 2022-04-22 01:45:19 +02:00
parent 775a805948
commit 96f0e82688
6 changed files with 648 additions and 5 deletions

View File

@ -287,6 +287,7 @@ FORMS += \
Traces/Math/timedomaingatingexplanationwidget.ui \ Traces/Math/timedomaingatingexplanationwidget.ui \
Traces/Math/timegatedialog.ui \ Traces/Math/timegatedialog.ui \
Traces/Math/timegateexplanationwidget.ui \ Traces/Math/timegateexplanationwidget.ui \
Traces/XYPlotConstantLineEditDialog.ui \
Traces/smithchartdialog.ui \ Traces/smithchartdialog.ui \
Traces/tracecsvexport.ui \ Traces/tracecsvexport.ui \
Traces/traceeditdialog.ui \ Traces/traceeditdialog.ui \

View File

@ -0,0 +1,206 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>XYPlotConstantLineEditDialog</class>
<widget class="QDialog" name="XYPlotConstantLineEditDialog">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>657</width>
<height>288</height>
</rect>
</property>
<property name="windowTitle">
<string>Dialog</string>
</property>
<property name="modal">
<bool>true</bool>
</property>
<layout class="QVBoxLayout" name="verticalLayout_3">
<item>
<layout class="QHBoxLayout" name="horizontalLayout_2" stretch="1,2">
<item>
<layout class="QVBoxLayout" name="verticalLayout_2">
<item>
<layout class="QFormLayout" name="formLayout">
<item row="0" column="0">
<widget class="QLabel" name="label">
<property name="text">
<string>Name:</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QLineEdit" name="name"/>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label_2">
<property name="text">
<string>Color:</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="ColorPickerButton" name="color">
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string/>
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QLabel" name="label_3">
<property name="text">
<string>Axis:</string>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="QComboBox" name="axis"/>
</item>
<item row="3" column="0">
<widget class="QLabel" name="label_4">
<property name="text">
<string>Pass/Fail:</string>
</property>
</widget>
</item>
<item row="3" column="1">
<widget class="QComboBox" name="passFail"/>
</item>
</layout>
</item>
<item>
<spacer name="verticalSpacer_2">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
<item>
<widget class="QGroupBox" name="groupBox">
<property name="title">
<string>Points</string>
</property>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QTableWidget" name="tableWidget">
<property name="selectionMode">
<enum>QAbstractItemView::SingleSelection</enum>
</property>
</widget>
</item>
<item>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QPushButton" name="addPoint">
<property name="text">
<string/>
</property>
<property name="icon">
<iconset theme="list-add" resource="../icons.qrc">
<normaloff>:/icons/add.png</normaloff>:/icons/add.png</iconset>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="removePoint">
<property name="text">
<string/>
</property>
<property name="icon">
<iconset theme="list-remove" resource="../icons.qrc">
<normaloff>:/icons/remove.png</normaloff>:/icons/remove.png</iconset>
</property>
</widget>
</item>
<item>
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
</layout>
</widget>
</item>
</layout>
</item>
<item>
<widget class="QDialogButtonBox" name="buttonBox">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="standardButtons">
<set>QDialogButtonBox::Ok</set>
</property>
</widget>
</item>
</layout>
</widget>
<customwidgets>
<customwidget>
<class>ColorPickerButton</class>
<extends>QPushButton</extends>
<header>CustomWidgets/colorpickerbutton.h</header>
</customwidget>
</customwidgets>
<resources>
<include location="../icons.qrc"/>
</resources>
<connections>
<connection>
<sender>buttonBox</sender>
<signal>accepted()</signal>
<receiver>XYPlotConstantLineEditDialog</receiver>
<slot>accept()</slot>
<hints>
<hint type="sourcelabel">
<x>248</x>
<y>254</y>
</hint>
<hint type="destinationlabel">
<x>157</x>
<y>274</y>
</hint>
</hints>
</connection>
<connection>
<sender>buttonBox</sender>
<signal>rejected()</signal>
<receiver>XYPlotConstantLineEditDialog</receiver>
<slot>reject()</slot>
<hints>
<hint type="sourcelabel">
<x>316</x>
<y>260</y>
</hint>
<hint type="destinationlabel">
<x>286</x>
<y>274</y>
</hint>
</hints>
</connection>
</connections>
</ui>

View File

@ -8,6 +8,7 @@
#include "unit.h" #include "unit.h"
#include "preferences.h" #include "preferences.h"
#include "appwindow.h" #include "appwindow.h"
#include "ui_XYPlotConstantLineEditDialog.h"
#include <QGridLayout> #include <QGridLayout>
#include <cmath> #include <cmath>
@ -32,6 +33,13 @@ TraceXYPlot::TraceXYPlot(TraceModel &model, QWidget *parent)
initializeTraceInfo(); initializeTraceInfo();
} }
TraceXYPlot::~TraceXYPlot()
{
for(auto l : constantLines) {
delete l;
}
}
void TraceXYPlot::setYAxis(int axis, YAxis::Type type, bool log, bool autorange, double min, double max, double div) void TraceXYPlot::setYAxis(int axis, YAxis::Type type, bool log, bool autorange, double min, double max, double div)
{ {
if(yAxis[axis].getType() != type) { if(yAxis[axis].getType() != type) {
@ -119,6 +127,11 @@ nlohmann::json TraceXYPlot::toJSON()
j["YSecondary"] = jY; j["YSecondary"] = jY;
} }
} }
nlohmann::json jlines;
for(auto l : constantLines) {
jlines.push_back(l->toJSON());
}
j["limitLines"] = jlines;
return j; return j;
} }
@ -174,6 +187,13 @@ void TraceXYPlot::fromJSON(nlohmann::json j)
} }
} }
} }
if(j.contains("limitLines")) {
for(auto jline : j["limitLines"]) {
auto line = new XYPlotConstantLine;
line->fromJSON(jline);
constantLines.push_back(line);
}
}
} }
bool TraceXYPlot::isTDRtype(YAxis::Type type) bool TraceXYPlot::isTDRtype(YAxis::Type type)
@ -330,6 +350,8 @@ void TraceXYPlot::draw(QPainter &p)
{ {
auto pref = Preferences::getInstance(); auto pref = Preferences::getInstance();
bool limitPassing = true;
auto w = p.window(); auto w = p.window();
auto pen = QPen(pref.Graphs.Color.axis, 0); auto pen = QPen(pref.Graphs.Color.axis, 0);
pen.setCosmetic(true); pen.setCosmetic(true);
@ -465,6 +487,19 @@ void TraceXYPlot::draw(QPainter &p)
} }
// draw line // draw line
p.drawLine(p1, p2); p.drawLine(p1, p2);
// checking limits
for(auto limit : constantLines) {
if(i == 0 && limit->getAxis() != XYPlotConstantLine::Axis::Primary) {
continue;
}
if(i == 1 && limit->getAxis() != XYPlotConstantLine::Axis::Secondary) {
continue;
}
if(!limit->pass(now)) {
limitPassing = false;
}
}
} }
if(i == 0 && nPoints > 0) { if(i == 0 && nPoints > 0) {
// only draw markers on primary YAxis and if the trace has at least one point // only draw markers on primary YAxis and if the trace has at least one point
@ -502,6 +537,35 @@ void TraceXYPlot::draw(QPainter &p)
} }
} }
} }
// plot constant lines
for(auto line : constantLines) {
// skip lines on wrong axis
if(i == 0 && line->getAxis() != XYPlotConstantLine::Axis::Primary) {
continue;
}
if(i == 1 && line->getAxis() != XYPlotConstantLine::Axis::Secondary) {
continue;
}
pen = QPen(line->getColor(), pref.Graphs.lineWidth);
pen.setCosmetic(true);
if(i == 1) {
pen.setStyle(Qt::DotLine);
} else {
pen.setStyle(Qt::SolidLine);
}
p.setPen(pen);
for(unsigned int j=1;j<line->getPoints().size();j++) {
// scale to plot coordinates
auto p1 = plotValueToPixel(line->getPoints()[j-1], i);
auto p2 = plotValueToPixel(line->getPoints()[j], i);
if(!plotRect.contains(p1) && !plotRect.contains(p2)) {
// completely out of frame
continue;
}
// draw line
p.drawLine(p1, p2);
}
}
p.setClipping(false); p.setClipping(false);
} }
@ -583,6 +647,8 @@ void TraceXYPlot::draw(QPainter &p)
} }
} }
// TODO check limitPassing
if(dropPending) { if(dropPending) {
p.setOpacity(0.5); p.setOpacity(0.5);
p.setBrush(Qt::white); p.setBrush(Qt::white);
@ -967,3 +1033,204 @@ QString TraceXYPlot::mouseText(QPoint pos)
} }
return ret; return ret;
} }
XYPlotConstantLine::XYPlotConstantLine()
: name("Name"),
color(Qt::red),
axis(Axis::Primary),
passFail(PassFail::DontCare),
points()
{
}
QColor XYPlotConstantLine::getColor() const
{
return color;
}
void XYPlotConstantLine::setColor(const QColor &value)
{
color = value;
}
void XYPlotConstantLine::fromJSON(nlohmann::json j)
{
name = QString::fromStdString(j.value("name", "Name"));
color = QColor(QString::fromStdString(j.value("color", "red")));
axis = AxisFromString(QString::fromStdString(j.value("axis", AxisToString(Axis::Primary).toStdString())));
if(axis == Axis::Last) {
axis = Axis::Primary;
}
passFail = PassFailFromString(QString::fromStdString(j.value("passfail", PassFailToString(PassFail::DontCare).toStdString())));
if(passFail == PassFail::Last) {
passFail = PassFail::DontCare;
}
points.clear();
if(j.contains("points")) {
for(auto jp : j["points"]) {
QPointF p;
p.setX(jp.value("x", 0.0));
p.setY(jp.value("y", 0.0));
points.push_back(p);
}
}
}
nlohmann::json XYPlotConstantLine::toJSON()
{
nlohmann::json j;
j["name"] = name.toStdString();
j["color"] = color.name().toStdString();
j["axis"] = AxisToString(axis).toStdString();
j["passfail"] = PassFailToString(passFail).toStdString();
nlohmann::json jpoints;
for(auto p : points) {
nlohmann::json jp;
jp["x"] = p.x();
jp["y"] = p.y();
jpoints.push_back(jp);
}
j["points"] = jpoints;
return j;
}
bool XYPlotConstantLine::pass(QPointF testPoint)
{
if(passFail == PassFail::DontCare) {
// always passes
return true;
}
if(points.size() < 2) {
// no lines, always passes
return true;
}
if(testPoint.x() < points.front().x() || testPoint.x() > points.back().x()) {
// out of range, always passes
return true;
}
auto p = lower_bound(points.begin(), points.end(), testPoint.x(), [](QPointF p, double x) -> bool {
return p.x() < x;
});
double compareY = 0.0;
if(p->x() == testPoint.x()) {
// Exact match
compareY = p->y();
} else {
// need to interpolate
auto high = p;
p--;
auto low = p;
double alpha = (testPoint.x() - low->x()) / (high->x() - low->x());
compareY = low->y() * (1 - alpha) + high->y() * alpha;
}
if (compareY < testPoint.y() && passFail == PassFail::HighLimit) {
// high limit exceeded
return false;
}
if (compareY > testPoint.y() && passFail == PassFail::LowLimit) {
// low limit exceeded
return false;
}
return true;
}
QString XYPlotConstantLine::AxisToString(Axis axis)
{
switch(axis) {
case Axis::Primary: return "Primary";
case Axis::Secondary: return "Secondary";
default: return "Invalid";
}
}
XYPlotConstantLine::Axis XYPlotConstantLine::AxisFromString(QString s)
{
for(unsigned int i=0;i<(unsigned int)Axis::Last;i++) {
if(AxisToString((Axis) i) == s) {
return (Axis) i;
}
}
return Axis::Last;
}
QString XYPlotConstantLine::PassFailToString(PassFail pf)
{
switch(pf) {
case PassFail::DontCare: return "Dont Care";
case PassFail::HighLimit: return "High limit";
case PassFail::LowLimit: return "Low limit";
default: return "Invalid";
}
}
XYPlotConstantLine::PassFail XYPlotConstantLine::PassFailFromString(QString s)
{
for(unsigned int i=0;i<(unsigned int)PassFail::Last;i++) {
if(PassFailToString((PassFail) i) == s) {
return (PassFail) i;
}
}
return PassFail::Last;
}
void XYPlotConstantLine::editDialog(QString xUnit, QString yUnitPrimary, QString yUnitSecondary)
{
auto d = new QDialog();
auto ui = new Ui_XYPlotConstantLineEditDialog;
ui->setupUi(d);
ui->name->setText(name);
ui->color->setColor(color);
for(unsigned int i=0;i<(unsigned int)Axis::Last;i++) {
ui->axis->addItem(AxisToString((Axis) i));
}
ui->axis->setCurrentIndex((int) axis);
for(unsigned int i=0;i<(unsigned int)PassFail::Last;i++) {
ui->passFail->addItem(PassFailToString((PassFail) i));
}
ui->passFail->setCurrentIndex((int) passFail);
connect(ui->name, &QLineEdit::textChanged, [=](){
name = ui->name->text();
});
connect(ui->color, &ColorPickerButton::colorChanged, [=](){
color = ui->color->getColor();
});
connect(ui->axis, qOverload<int>(&QComboBox::currentIndexChanged), [=](){
axis = (Axis) ui->axis->currentIndex();
// TODO apply unit change
});
connect(ui->passFail, qOverload<int>(&QComboBox::currentIndexChanged), [=](){
passFail = (PassFail) ui->passFail->currentIndex();
});
// TODO handle adding/removing of points
connect(d, &QDialog::finished, this, &XYPlotConstantLine::editingFinished);
if(AppWindow::showGUI()) {
d->show();
}
}
QString XYPlotConstantLine::getDescription()
{
QString ret;
ret += name;
ret += ", " + QString::number(points.size()) + " points, limit: "+PassFailToString(passFail);
return ret;
}
XYPlotConstantLine::Axis XYPlotConstantLine::getAxis() const
{
return axis;
}
const std::vector<QPointF> &XYPlotConstantLine::getPoints() const
{
return points;
}

View File

@ -6,12 +6,60 @@
#include <set> #include <set>
class XYPlotConstantLine : public QObject, public Savable
{
Q_OBJECT
public:
enum class Axis {
Primary,
Secondary,
Last,
};
enum class PassFail {
DontCare,
HighLimit,
LowLimit,
Last,
};
XYPlotConstantLine();
QColor getColor() const;
void setColor(const QColor &value);
void fromJSON(nlohmann::json j) override;
nlohmann::json toJSON() override;
bool pass(QPointF testPoint);
static QString AxisToString(Axis axis);
static Axis AxisFromString(QString s);
static QString PassFailToString(PassFail pf);
static PassFail PassFailFromString(QString s);
void editDialog(QString xUnit, QString yUnitPrimary, QString yUnitSecondary);
QString getDescription();
Axis getAxis() const;
const std::vector<QPointF> &getPoints() const;
signals:
void editingFinished();
private:
QString name;
QColor color;
Axis axis;
PassFail passFail;
std::vector<QPointF> points;
};
class TraceXYPlot : public TracePlot class TraceXYPlot : public TracePlot
{ {
friend class XYplotAxisDialog; friend class XYplotAxisDialog;
Q_OBJECT Q_OBJECT
public: public:
TraceXYPlot(TraceModel &model, QWidget *parent = nullptr); TraceXYPlot(TraceModel &model, QWidget *parent = nullptr);
~TraceXYPlot();
enum class XAxisMode { enum class XAxisMode {
UseSpan, UseSpan,
@ -69,6 +117,8 @@ private:
XAxisMode xAxisMode; XAxisMode xAxisMode;
int plotAreaLeft, plotAreaWidth, plotAreaBottom, plotAreaTop; int plotAreaLeft, plotAreaWidth, plotAreaBottom, plotAreaTop;
std::vector<XYPlotConstantLine*> constantLines;
}; };
#endif // TRACEXYPLOT_H #endif // TRACEXYPLOT_H

View File

@ -156,6 +156,47 @@ XYplotAxisDialog::XYplotAxisDialog(TraceXYPlot *plot) :
ui->Xmin->setValueQuiet(plot->xAxis.getRangeMin()); ui->Xmin->setValueQuiet(plot->xAxis.getRangeMin());
ui->Xmax->setValueQuiet(plot->xAxis.getRangeMax()); ui->Xmax->setValueQuiet(plot->xAxis.getRangeMax());
ui->Xdivs->setValueQuiet(plot->xAxis.getRangeDiv()); ui->Xdivs->setValueQuiet(plot->xAxis.getRangeDiv());
// Constant line list handling
auto editLine = [&](XYPlotConstantLine *line) {
line->editDialog(XAxis::Unit((XAxis::Type) ui->XType->currentIndex()),
YAxis::Unit((YAxis::Type) ui->Y1type->currentIndex()),
YAxis::Unit((YAxis::Type) ui->Y2type->currentIndex()));
};
for(auto l : plot->constantLines) {
ui->lineList->addItem(l->getDescription());
}
if(plot->constantLines.size() > 0) {
ui->removeLine->setEnabled(true);
}
connect(ui->addLine, &QPushButton::clicked, [=](){
auto line = new XYPlotConstantLine();
plot->constantLines.push_back(line);
auto item = new QListWidgetItem(line->getDescription());
ui->lineList->addItem(item);
ui->lineList->setCurrentItem(item);
ui->removeLine->setEnabled(true);
editLine(line);
connect(line, &XYPlotConstantLine::editingFinished, [=](){
item->setText(line->getDescription());
});
});
connect(ui->removeLine, &QPushButton::clicked, [=](){
auto index = ui->lineList->currentRow();
delete ui->lineList->takeItem(index);
delete plot->constantLines[index];
plot->constantLines.erase(plot->constantLines.begin() + index);
if(plot->constantLines.size() == 0) {
ui->removeLine->setEnabled(false);
}
});
connect(ui->lineList, &QListWidget::itemDoubleClicked, [=](QListWidgetItem *item){
ui->lineList->setCurrentItem(item);
editLine(plot->constantLines[ui->lineList->currentRow()]);
});
} }
XYplotAxisDialog::~XYplotAxisDialog() XYplotAxisDialog::~XYplotAxisDialog()

View File

@ -9,8 +9,8 @@
<rect> <rect>
<x>0</x> <x>0</x>
<y>0</y> <y>0</y>
<width>773</width> <width>814</width>
<height>282</height> <height>439</height>
</rect> </rect>
</property> </property>
<property name="windowTitle"> <property name="windowTitle">
@ -19,7 +19,7 @@
<property name="modal"> <property name="modal">
<bool>true</bool> <bool>true</bool>
</property> </property>
<layout class="QVBoxLayout" name="verticalLayout_4"> <layout class="QVBoxLayout" name="verticalLayout_5">
<item> <item>
<layout class="QHBoxLayout" name="horizontalLayout_5"> <layout class="QHBoxLayout" name="horizontalLayout_5">
<item> <item>
@ -431,6 +431,82 @@
</item> </item>
</layout> </layout>
</item> </item>
<item>
<widget class="QGroupBox" name="groupBox">
<property name="title">
<string>Constant Lines</string>
</property>
<layout class="QHBoxLayout" name="horizontalLayout_6">
<item>
<widget class="QListWidget" name="lineList">
<property name="selectionMode">
<enum>QAbstractItemView::SingleSelection</enum>
</property>
</widget>
</item>
<item>
<layout class="QVBoxLayout" name="verticalLayout_4">
<item>
<widget class="QPushButton" name="addLine">
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="toolTip">
<string>Add</string>
</property>
<property name="text">
<string/>
</property>
<property name="icon">
<iconset theme="list-add" resource="../icons.qrc">
<normaloff>:/icons/add.png</normaloff>:/icons/add.png</iconset>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="removeLine">
<property name="enabled">
<bool>false</bool>
</property>
<property name="sizePolicy">
<sizepolicy hsizetype="Minimum" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="toolTip">
<string>Delete</string>
</property>
<property name="text">
<string/>
</property>
<property name="icon">
<iconset theme="list-remove" resource="../icons.qrc">
<normaloff>:/icons/remove.png</normaloff>:/icons/remove.png</iconset>
</property>
</widget>
</item>
<item>
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>18</width>
<height>186</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
</layout>
</widget>
</item>
<item> <item>
<widget class="QDialogButtonBox" name="buttonBox"> <widget class="QDialogButtonBox" name="buttonBox">
<property name="orientation"> <property name="orientation">
@ -450,7 +526,9 @@
<header>CustomWidgets/siunitedit.h</header> <header>CustomWidgets/siunitedit.h</header>
</customwidget> </customwidget>
</customwidgets> </customwidgets>
<resources/> <resources>
<include location="../icons.qrc"/>
</resources>
<connections> <connections>
<connection> <connection>
<sender>buttonBox</sender> <sender>buttonBox</sender>
@ -486,8 +564,8 @@
</connection> </connection>
</connections> </connections>
<buttongroups> <buttongroups>
<buttongroup name="Y1group"/>
<buttongroup name="Y2group"/> <buttongroup name="Y2group"/>
<buttongroup name="Y1group"/>
<buttongroup name="Xgroup"/> <buttongroup name="Xgroup"/>
</buttongroups> </buttongroups>
</ui> </ui>