summaryrefslogtreecommitdiff
path: root/wx+/graph.cpp
diff options
context:
space:
mode:
authorDaniel Wilhelm <daniel@wili.li>2014-04-18 17:26:50 +0200
committerDaniel Wilhelm <daniel@wili.li>2014-04-18 17:26:50 +0200
commit669df123648aaa6aeccc70206b5417bc48b4e9ae (patch)
tree463c107a8d6405020bb304f7a7253e6b64afeee0 /wx+/graph.cpp
parent5.18 (diff)
downloadFreeFileSync-669df123648aaa6aeccc70206b5417bc48b4e9ae.tar.gz
FreeFileSync-669df123648aaa6aeccc70206b5417bc48b4e9ae.tar.bz2
FreeFileSync-669df123648aaa6aeccc70206b5417bc48b4e9ae.zip
5.19
Diffstat (limited to 'wx+/graph.cpp')
-rw-r--r--wx+/graph.cpp495
1 files changed, 360 insertions, 135 deletions
diff --git a/wx+/graph.cpp b/wx+/graph.cpp
index 0d14ae69..cbedfa53 100644
--- a/wx+/graph.cpp
+++ b/wx+/graph.cpp
@@ -9,8 +9,9 @@
#include <algorithm>
#include <numeric>
#include <zen/basic_math.h>
+#include <zen/scope_guard.h>
#include <wx/settings.h>
-#include "rtl.h"
+#include "dc.h"
using namespace zen;
@@ -31,8 +32,8 @@ double zen::nextNiceNumber(double blockSize) //round to next number which is a c
const double e = std::pow(10, k);
if (numeric::isNull(e))
return 0;
- const double a = blockSize / e; //blockSize = a * 10^k with a in (1, 10)
- assert(1 < a && a < 10);
+ const double a = blockSize / e; //blockSize = a * 10^k with a in [1, 10)
+ assert(1 <= a && a < 10);
//have a look at leading two digits: "nice" numbers start with 1, 2, 2.5 and 5
const double steps[] = { 1, 2, 2.5, 5, 10 };
@@ -78,18 +79,22 @@ public:
ConvertCoord(double valMin, double valMax, size_t screenSize) :
min_(valMin),
scaleToReal(screenSize == 0 ? 0 : (valMax - valMin) / screenSize),
- scaleToScr(numeric::isNull((valMax - valMin)) ? 0 : screenSize / (valMax - valMin)) {}
+ scaleToScr(numeric::isNull((valMax - valMin)) ? 0 : screenSize / (valMax - valMin)),
+ outOfBoundsLow (-1 * scaleToReal + valMin),
+ outOfBoundsHigh((screenSize + 1) * scaleToReal + valMin) { if (outOfBoundsLow > outOfBoundsHigh) std::swap(outOfBoundsLow, outOfBoundsHigh); }
double screenToReal(double screenPos) const //input value: [0, screenSize - 1]
{
- return screenPos * scaleToReal + min_; //come close to valMax, but NEVER reach it!
+ return screenPos * scaleToReal + min_;
}
double realToScreen(double realPos) const //return screen position in pixel (but with double precision!)
{
return (realPos - min_) * scaleToScr;
}
- int realToScreenRound(double realPos) const //useful to find "proper" y-pixel positions
+ int realToScreenRound(double realPos) const //returns -1 and screenSize + 1 if out of bounds!
{
+ //catch large double values: if double is larger than what int can represent => undefined behavior!
+ numeric::confine(realPos , outOfBoundsLow, outOfBoundsHigh);
return numeric::round(realToScreen(realPos));
}
@@ -97,19 +102,22 @@ private:
double min_;
double scaleToReal;
double scaleToScr;
+
+ double outOfBoundsLow;
+ double outOfBoundsHigh;
};
//enlarge value range to display to a multiple of a "useful" block size
void widenRange(double& valMin, double& valMax, //in/out
int& blockCount, //out
- int graphAreaSize, //in pixel
- int optimalBlockSize, //
+ int graphAreaSize, //in pixel
+ int optimalBlockSizePx, //
const LabelFormatter& labelFmt)
{
if (graphAreaSize > 0)
{
- double valRangePerBlock = (valMax - valMin) * optimalBlockSize / graphAreaSize; //proposal
+ double valRangePerBlock = (valMax - valMin) * optimalBlockSizePx / graphAreaSize; //proposal
valRangePerBlock = labelFmt.getOptimalBlockSize(valRangePerBlock);
if (!numeric::isNull(valRangePerBlock))
{
@@ -145,7 +153,7 @@ void drawXLabel(wxDC& dc, double xMin, double xMax, int blockCount, const Conver
//draw x axis labels
const wxString label = labelFmt.formatText(valX, valRangePerBlock);
- wxSize labelExtent = dc.GetMultiLineTextExtent(label);
+ const wxSize labelExtent = dc.GetMultiLineTextExtent(label);
dc.DrawText(label, wxPoint(x - labelExtent.GetWidth() / 2, labelArea.y + (labelArea.height - labelExtent.GetHeight()) / 2)); //center
}
}
@@ -173,31 +181,223 @@ void drawYLabel(wxDC& dc, double yMin, double yMax, int blockCount, const Conver
//draw y axis labels
const wxString label = labelFmt.formatText(valY, valRangePerBlock);
- wxSize labelExtent = dc.GetMultiLineTextExtent(label);
+ const wxSize labelExtent = dc.GetMultiLineTextExtent(label);
dc.DrawText(label, wxPoint(labelArea.x + (labelArea.width - labelExtent.GetWidth()) / 2, y - labelExtent.GetHeight() / 2)); //center
}
}
-template <class StdContainter>
-void subsample(StdContainter& cont, size_t factor)
+void drawCornerText(wxDC& dc, const wxRect& graphArea, const wxString& txt, Graph2D::PosCorner pos)
{
- if (factor <= 1) return;
+ if (txt.empty()) return;
+ const int borderX = 5;
+ const int borderY = 2; //it looks like wxDC::GetMultiLineTextExtent() precisely returns width, but too large a height: maybe they consider "text row height"?
- auto itOut = cont.begin();
- for (auto itIn = cont.begin(); cont.end() - itIn >= static_cast<ptrdiff_t>(factor); itIn += factor) //don't even let iterator point out of range!
- *itOut++ = std::accumulate(itIn, itIn + factor, 0.0) / static_cast<double>(factor);
+ wxDCTextColourChanger dummy(dc, wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOWTEXT)); //use user setting for labels
+ wxSize txtExtent = dc.GetMultiLineTextExtent(txt);
+ txtExtent.x += 2 * borderX;
+ txtExtent.y += 2 * borderY;
- cont.erase(itOut, cont.end());
+ wxPoint drawPos = graphArea.GetTopLeft();
+ switch (pos)
+ {
+ case Graph2D::CORNER_TOP_LEFT:
+ break;
+ case Graph2D::CORNER_TOP_RIGHT:
+ drawPos.x += graphArea.width - txtExtent.GetWidth();
+ break;
+ case Graph2D::CORNER_BOTTOM_LEFT:
+ drawPos.y += graphArea.height - txtExtent.GetHeight();
+ break;
+ case Graph2D::CORNER_BOTTOM_RIGHT:
+ drawPos.x += graphArea.width - txtExtent.GetWidth();
+ drawPos.y += graphArea.height - txtExtent.GetHeight();
+ break;
+ }
+ dc.DrawText(txt, drawPos + wxPoint(borderX, borderY));
}
+
+
+warn_static("review")
+
+template <class Function, class Function2>
+void cutPoints(std::vector<CurvePoint>& curvePoints, std::vector<char>& oobMarker, Function isInside, Function2 getIntersection)
+{
+ assert(curvePoints.size() == oobMarker.size());
+ if (curvePoints.size() != oobMarker.size() || curvePoints.empty()) return;
+ auto isMarkedOob = [&](size_t index) { return oobMarker[index] != 0; };
+
+ std::vector<CurvePoint> curvePointsTmp;
+ std::vector<char> oobMarkerTmp;
+ auto savePoint = [&](const CurvePoint& pt, bool markedOob) { curvePointsTmp.push_back(pt); oobMarkerTmp.push_back(markedOob); };
+
+ warn_static("perf: avoid these push_backs")
+
+ bool lastPointInside = isInside(curvePoints[0]);
+ if (lastPointInside)
+ savePoint(curvePoints[0], isMarkedOob(0));
+
+ for (auto it = curvePoints.begin() + 1; it != curvePoints.end(); ++it)
+ {
+ const size_t index = it - curvePoints.begin();
+
+ const bool pointInside = isInside(*it);
+ if (pointInside != lastPointInside)
+ {
+ lastPointInside = pointInside;
+
+ const CurvePoint is = getIntersection(*(it - 1), *it); //getIntersection returns *it when delta is zero
+ savePoint(is, !pointInside || isMarkedOob(index - 1));
+ }
+ if (pointInside)
+ savePoint(*it, isMarkedOob(index));
+ }
+ curvePointsTmp.swap(curvePoints);
+ oobMarkerTmp .swap(oobMarker);
+}
+
+
+struct GetIntersectionX
+{
+ GetIntersectionX(double x) : x_(x) {}
+ CurvePoint operator()(const CurvePoint& from, const CurvePoint& to) const
+ {
+ const double deltaX = to.x - from.x;
+ const double deltaY = to.y - from.y;
+ return !numeric::isNull(deltaX) ? CurvePoint(x_, from.y + (x_ - from.x) / deltaX * deltaY) : to;
+ };
+private:
+ double x_;
+};
+
+struct GetIntersectionY
+{
+ GetIntersectionY(double y) : y_(y) {}
+ CurvePoint operator()(const CurvePoint& from, const CurvePoint& to) const
+ {
+ const double deltaX = to.x - from.x;
+ const double deltaY = to.y - from.y;
+ return !numeric::isNull(deltaY) ? CurvePoint(from.x + (y_ - from.y) / deltaY * deltaX, y_) : to;
+ };
+private:
+ double y_;
+};
+
+void cutPointsOutsideX(std::vector<CurvePoint>& curvePoints, std::vector<char>& oobMarker, double minX, double maxX)
+{
+ cutPoints(curvePoints, oobMarker, [&](const CurvePoint& pt) { return pt.x >= minX; }, GetIntersectionX(minX));
+ cutPoints(curvePoints, oobMarker, [&](const CurvePoint& pt) { return pt.x <= maxX; }, GetIntersectionX(maxX));
+}
+
+void cutPointsOutsideY(std::vector<CurvePoint>& curvePoints, std::vector<char>& oobMarker, double minY, double maxY)
+{
+ cutPoints(curvePoints, oobMarker, [&](const CurvePoint& pt) { return pt.y >= minY; }, GetIntersectionY(minY));
+ cutPoints(curvePoints, oobMarker, [&](const CurvePoint& pt) { return pt.y <= maxY; }, GetIntersectionY(maxY));
+}
+}
+
+
+warn_static("review")
+void ContinuousCurveData::getPoints(double minX, double maxX, int pixelWidth, std::vector<CurvePoint>& points) const
+{
+ if (pixelWidth <= 1) return;
+ const ConvertCoord cvrtX(minX, maxX, pixelWidth - 1); //map [minX, maxX] to [0, pixelWidth - 1]
+
+ std::pair<double, double> rangeX = getRangeX();
+ //catch large double values: if double is larger than what int can represent => undefined behavior!
+ const double xOutOfBoundsLow = cvrtX.screenToReal(-1);
+ const double xOutOfBoundsHigh = cvrtX.screenToReal(pixelWidth);
+ numeric::confine(rangeX.first , xOutOfBoundsLow, xOutOfBoundsHigh); //don't confine to [minX, maxX] which
+ numeric::confine(rangeX.second, xOutOfBoundsLow, xOutOfBoundsHigh); //would prevent empty ranges
+
+ const int posFrom = std::ceil (cvrtX.realToScreen(std::max(rangeX.first, minX))); //do not step outside [minX, maxX]
+ const int posTo = std::floor(cvrtX.realToScreen(std::min(rangeX.second, maxX))); //
+ //conversion from std::floor/std::ceil double return value to int is loss-free for full value range of 32-bit int! tested successfully on MSVC
+
+ for (int i = posFrom; i <= posTo; ++i)
+ {
+ const double x = cvrtX.screenToReal(i);
+ points.push_back(CurvePoint(x, getValue(x)));
+ }
+}
+
+
+void SparseCurveData::getPoints(double minX, double maxX, int pixelWidth, std::vector<CurvePoint>& points) const
+{
+ if (pixelWidth <= 1) return;
+ const ConvertCoord cvrtX(minX, maxX, pixelWidth - 1); //map [minX, maxX] to [0, pixelWidth - 1]
+ const std::pair<double, double> rangeX = getRangeX();
+
+ auto addPoint = [&](const CurvePoint& pt)
+ {
+ warn_static("verify steps")
+ if (addSteps_ && !points.empty())
+ if (pt.y != points.back().y)
+ points.push_back(CurvePoint(pt.x, points.back().y));
+ points.push_back(pt);
+ };
+
+ const int posFrom = cvrtX.realToScreenRound(std::max(rangeX.first, minX));
+ const int posTo = cvrtX.realToScreenRound(std::min(rangeX.second, maxX));
+
+ for (int i = posFrom; i <= posTo; ++i)
+ {
+ const double x = cvrtX.screenToReal(i);
+ Opt<CurvePoint> ptLe = getLessEq(x);
+ Opt<CurvePoint> ptGe = getGreaterEq(x);
+ //both non-existent and invalid return values are mapped to out of expected range: => check on posLe/posGe NOT ptLe/ptGE in the following!
+ const int posLe = ptLe ? cvrtX.realToScreenRound(ptLe->x) : i + 1;
+ const int posGe = ptGe ? cvrtX.realToScreenRound(ptGe->x) : i - 1;
+ assert(!ptLe || posLe <= i); //check for invalid return values
+ assert(!ptGe || posGe >= i); //
+
+ if (posGe == i) //test if point would be mapped to pixel x-position i
+ {
+ if (posLe == i) //
+ addPoint(x - ptLe->x < ptGe->x - x ? *ptLe : *ptGe);
+ else
+ addPoint(*ptGe);
+ }
+ else
+ {
+ if (posLe == i)
+ addPoint(*ptLe);
+ else //no point for x-position i
+ {
+ if (i == posFrom && posGe > i)
+ {
+ if (posLe < i)
+ addPoint(*ptLe); //use first point outside display area!
+ else if (posGe > posTo) //curve starts outside the draw range!
+ break;
+ }
+ }
+
+ if (posGe < i)
+ break;
+
+ if (posGe > posTo) //last point outside the display area!
+ {
+ if (i == posTo && posLe == i) //no need for outside point if last position was already set above
+ break;
+
+ addPoint(*ptGe);
+ break;
+ }
+ if (posGe > i) //skip sparse area
+ i = posGe - 1;
+ }
+ }
}
+
Graph2D::Graph2D(wxWindow* parent,
wxWindowID winid,
const wxPoint& pos,
const wxSize& size,
long style,
- const wxString& name) : wxPanel(parent, winid, pos, size, style, name)
+ const wxString& name) : wxPanel(parent, winid, pos, size, style, name),
+ labelFont(wxNORMAL_FONT->GetPointSize(), wxFONTFAMILY_DEFAULT, wxFONTSTYLE_NORMAL, wxFONTWEIGHT_NORMAL, false, L"Arial")
{
Connect(wxEVT_PAINT, wxPaintEventHandler(Graph2D::onPaintEvent), nullptr, this);
Connect(wxEVT_SIZE, wxSizeEventHandler (Graph2D::onSizeEvent ), nullptr, this);
@@ -272,14 +472,14 @@ void Graph2D::OnMouseCaptureLost(wxMouseCaptureLostEvent& event)
}
-void Graph2D::setData(const std::shared_ptr<GraphData>& data, const CurveAttributes& ca)
+void Graph2D::setCurve(const std::shared_ptr<CurveData>& data, const CurveAttributes& ca)
{
curves_.clear();
- addData(data, ca);
+ addCurve(data, ca);
}
-void Graph2D::addData(const std::shared_ptr<GraphData>& data, const CurveAttributes& ca)
+void Graph2D::addCurve(const std::shared_ptr<CurveData>& data, const CurveAttributes& ca)
{
CurveAttributes newAttr = ca;
if (newAttr.autoColor)
@@ -288,22 +488,13 @@ void Graph2D::addData(const std::shared_ptr<GraphData>& data, const CurveAttribu
Refresh();
}
-namespace //putting this into function scope makes MSVC crash...
-{
-struct CurveSamples
-{
- CurveSamples() : offsetX(0) {}
- std::vector<double> yValues; //actual y-values at each screen pixel position
- int offsetX; //x-value offset in pixels
-};
-}
void Graph2D::render(wxDC& dc) const
{
using namespace numeric;
//set label font right at the start so that it is considered by wxDC::GetTextExtent below!
- dc.SetFont(wxFont(wxNORMAL_FONT->GetPointSize(), wxFONTFAMILY_DEFAULT, wxFONTSTYLE_NORMAL, wxFONTWEIGHT_NORMAL, false, L"Arial"));
+ dc.SetFont(labelFont);
const wxRect clientRect = GetClientRect(); //DON'T use wxDC::GetSize()! DC may be larger than visible area!
{
@@ -365,27 +556,25 @@ void Graph2D::render(wxDC& dc) const
//set label areas respecting graph area border!
const wxRect xLabelArea(graphArea.x, xLabelPosY, graphArea.width, attr.xLabelHeight);
const wxRect yLabelArea(yLabelPosX, graphArea.y, attr.yLabelWidth, graphArea.height);
-
const wxPoint graphAreaOrigin = graphArea.GetTopLeft();
//detect x value range
double minX = attr.minXauto ? std::numeric_limits<double>::infinity() : attr.minX; //automatic: ensure values are initialized by first curve
double maxX = attr.maxXauto ? -std::numeric_limits<double>::infinity() : attr.maxX; //
- if (!curves_.empty())
- for (auto it = curves_.begin(); it != curves_.end(); ++it)
- if (it->first.get())
- {
- const GraphData& graph = *it->first;
- assert(graph.getXBegin() <= graph.getXEnd() + 1.0e-9);
- //GCC fucks up badly when comparing two *binary identical* doubles and finds "begin > end" with diff of 1e-18
-
- if (attr.minXauto)
- minX = std::min(minX, graph.getXBegin());
- if (attr.maxXauto)
- maxX = std::max(maxX, graph.getXEnd());
- }
+ for (auto it = curves_.begin(); it != curves_.end(); ++it)
+ if (const CurveData* curve = it->first.get())
+ {
+ const std::pair<double, double> rangeX = curve->getRangeX();
+ assert(rangeX.first <= rangeX.second + 1.0e-9);
+ //GCC fucks up badly when comparing two *binary identical* doubles and finds "begin > end" with diff of 1e-18
+
+ if (attr.minXauto)
+ minX = std::min(minX, rangeX.first);
+ if (attr.maxXauto)
+ maxX = std::max(maxX, rangeX.second);
+ }
- if (minX < maxX && maxX - minX < std::numeric_limits<double>::infinity()) //valid x-range
+ if (minX <= maxX && maxX - minX < std::numeric_limits<double>::infinity()) //valid x-range
{
int blockCountX = 0;
//enlarge minX, maxX to a multiple of a "useful" block size
@@ -396,39 +585,37 @@ void Graph2D::render(wxDC& dc) const
dc.GetTextExtent(L"100000000000000").GetWidth(),
*attr.labelFmtX);
- //detect y value range
- std::vector<CurveSamples> yValuesList(curves_.size());
+ //get raw values + detect y value range
double minY = attr.minYauto ? std::numeric_limits<double>::infinity() : attr.minY; //automatic: ensure values are initialized by first curve
double maxY = attr.maxYauto ? -std::numeric_limits<double>::infinity() : attr.maxY; //
- {
- const int AVG_FACTOR = 2; //some averaging of edgy input data to smoothen behavior on window resize
- const ConvertCoord cvrtX(minX, maxX, graphArea.width * AVG_FACTOR);
- for (auto it = curves_.begin(); it != curves_.end(); ++it)
- if (const GraphData* graph = it->first.get())
- {
- CurveSamples& samples = yValuesList[it - curves_.begin()];
- {
- const int posFirst = std::ceil(cvrtX.realToScreen(std::max(graph->getXBegin(), minX))); //do not step outside [xBegin, xEnd) range => 2 x ceil!
- const int posLast = std::ceil(cvrtX.realToScreen(std::min(graph->getXEnd (), maxX))); //
- //conversion from std::ceil double return valute to int is loss-free for full value range of 32-bit int! tested successfully on MSVC
+ std::vector<std::vector<CurvePoint>> curvePoints(curves_.size());
+ std::vector<std::vector<char>> oobMarker (curves_.size()); //effectively a std::vector<bool> marking points that start a out of bounds line
- for (int i = posFirst; i < posLast; ++i)
- samples.yValues.push_back(graph->getValue(cvrtX.screenToReal(i)));
+ for (auto it = curves_.begin(); it != curves_.end(); ++it)
+ if (const CurveData* curve = it->first.get())
+ {
+ const size_t index = it - curves_.begin();
+ std::vector<CurvePoint>& points = curvePoints[index];
+ auto& marker = oobMarker[index];
- subsample(samples.yValues, AVG_FACTOR);
- samples.offsetX = posFirst / AVG_FACTOR;
- }
- if (!samples.yValues.empty())
- {
- if (attr.minYauto)
- minY = std::min(minY, *std::min_element(samples.yValues.begin(), samples.yValues.end()));
- if (attr.maxYauto)
- maxY = std::max(maxY, *std::max_element(samples.yValues.begin(), samples.yValues.end()));
- }
+ curve->getPoints(minX, maxX, graphArea.width, points);
+
+ //cut points outside visible x-range now in order to calculate height of visible points only!
+ marker.resize(points.size()); //default value: false
+ cutPointsOutsideX(points, marker, minX, maxX);
+
+ if ((attr.minYauto || attr.maxYauto) && !points.empty())
+ {
+ auto itPair = std::minmax_element(points.begin(), points.end(), [](const CurvePoint& lhs, const CurvePoint& rhs) { return lhs.y < rhs.y; });
+ if (attr.minYauto)
+ minY = std::min(minY, itPair.first->y);
+ if (attr.maxYauto)
+ maxY = std::max(maxY, itPair.second->y);
}
- }
- if (minY < maxY) //valid y-range
+ }
+
+ if (minY <= maxY) //valid y-range
{
int blockCountY = 0;
//enlarge minY, maxY to a multiple of a "useful" block size
@@ -439,44 +626,55 @@ void Graph2D::render(wxDC& dc) const
3 * dc.GetTextExtent(L"1").GetHeight(),
*attr.labelFmtY);
- const ConvertCoord cvrtX(minX, maxX, graphArea.width); //map [minX, maxX) to [0, graphWidth)
- const ConvertCoord cvrtY(maxY, minY, graphArea.height <= 0 ? 0 : graphArea.height - 1); //map [minY, maxY] to [graphHeight - 1, 0]
+ if (graphArea.width <= 1 || graphArea.height <= 1) return;
+ const ConvertCoord cvrtX(minX, maxX, graphArea.width - 1); //map [minX, maxX] to [0, pixelWidth - 1]
+ const ConvertCoord cvrtY(maxY, minY, graphArea.height - 1); //map [minY, maxY] to [pixelHeight - 1, 0]
//calculate curve coordinates on graph area
- auto getCurvePoints = [&](size_t index, std::vector<wxPoint>& points)
- {
- if (index < yValuesList.size())
- {
- CurveSamples& samples = yValuesList[index];
+ std::vector<std::vector<wxPoint>> drawPoints(curves_.size());
- for (auto it = samples.yValues.begin(); it != samples.yValues.end(); ++it)
- points.push_back(wxPoint(samples.offsetX + (it - samples.yValues.begin()),
- cvrtY.realToScreenRound(*it)) + graphAreaOrigin);
- }
- };
+ for (size_t index = 0; index < curves_.size(); ++index)
+ {
+ //cut points outside visible y-range before calculating pixels:
+ //1. realToScreenRound() deforms out-of-range values!
+ //2. pixels that are grossly out of range may become a severe performance problem when drawing on the DC (Windows)
+ cutPointsOutsideY(curvePoints[index], oobMarker[index], minY, maxY);
+
+ auto& points = drawPoints[index];
+ for (const auto& pt : curvePoints[index])
+ points.push_back(wxPoint(cvrtX.realToScreenRound(pt.x),
+ cvrtY.realToScreenRound(pt.y)) + graphAreaOrigin);
+ }
//update active mouse selection
if (activeSel.get() &&
graphArea.width > 0 && graphArea.height > 0)
{
- wxPoint startPos = activeSel->getStartPos() - graphAreaOrigin; //make relative to graphArea
- wxPoint currentPos = activeSel->refCurrentPos() - graphAreaOrigin;
-
- //normalize positions: a mouse selection is symmetric and *not* an half-open range!
- confine(startPos .x, 0, graphArea.width - 1);
- confine(currentPos.x, 0, graphArea.width - 1);
- confine(startPos .y, 0, graphArea.height - 1);
- confine(currentPos.y, 0, graphArea.height - 1);
-
- auto& from = activeSel->refSelection().from;
- auto& to = activeSel->refSelection().to;
+ auto widen = [](double* low, double* high)
+ {
+ if (*low > *high)
+ std::swap(low, high);
+ *low -= 0.5;
+ *high += 0.5;
+ };
- //save current selection as double coordinates
- from.x = cvrtX.screenToReal(startPos .x + (startPos.x <= currentPos.x ? 0 : 1)); // use full pixel range for selection!
- to .x = cvrtX.screenToReal(currentPos.x + (startPos.x <= currentPos.x ? 1 : 0));
+ const wxPoint screenStart = activeSel->getStartPos() - graphAreaOrigin; //make relative to graphArea
+ const wxPoint screenCurrent = activeSel->refCurrentPos() - graphAreaOrigin;
- from.y = cvrtY.screenToReal(startPos .y + (startPos.y <= currentPos.y ? 0 : 1));
- to .y = cvrtY.screenToReal(currentPos.y + (startPos.y <= currentPos.y ? 1 : 0));
+ //normalize positions: a mouse selection is symmetric and *not* an half-open range!
+ double screenFromX = confineCpy(screenStart .x, 0, graphArea.width - 1);
+ double screenFromY = confineCpy(screenStart .y, 0, graphArea.height - 1);
+ double screenToX = confineCpy(screenCurrent.x, 0, graphArea.width - 1);
+ double screenToY = confineCpy(screenCurrent.y, 0, graphArea.height - 1);
+ widen(&screenFromX, &screenToX); //use full pixel range for selection!
+ widen(&screenFromY, &screenToY);
+
+ //save current selection as "double" coordinates
+ activeSel->refSelection().from = CurvePoint(cvrtX.screenToReal(screenFromX),
+ cvrtY.screenToReal(screenFromY));
+
+ activeSel->refSelection().to = CurvePoint(cvrtX.screenToReal(screenToX),
+ cvrtY.screenToReal(screenToY));
}
//#################### begin drawing ####################
@@ -484,8 +682,7 @@ void Graph2D::render(wxDC& dc) const
for (auto it = curves_.begin(); it != curves_.end(); ++it)
if (it->second.drawCurveArea)
{
- std::vector<wxPoint> points;
- getCurvePoints(it - curves_.begin(), points);
+ std::vector<wxPoint> points = drawPoints[it - curves_.begin()];
if (!points.empty())
{
points.push_back(wxPoint(points.back ().x, graphArea.GetBottom())); //add lower right and left corners
@@ -502,36 +699,39 @@ void Graph2D::render(wxDC& dc) const
if (activeSel)
allSelections.push_back(activeSel->refSelection());
{
- //alpha channel (not yet) supported on wxMSW, so draw selection before curves
+ //alpha channel not supported on wxMSW, so draw selection before curves
wxDCBrushChanger dummy(dc, wxColor(168, 202, 236)); //light blue
wxDCPenChanger dummy2(dc, wxColor(51, 153, 255)); //dark blue
- for (auto it = allSelections.begin(); it != allSelections.end(); ++it)
+ auto shrink = [](double* low, double* high)
{
- //harmonize with active mouse selection above!
- wxPoint pixelFrom(cvrtX.realToScreenRound(it->from.x),
- cvrtY.realToScreenRound(it->from.y));
- wxPoint pixelTo(cvrtX.realToScreenRound(it->to.x),
- cvrtY.realToScreenRound(it->to.y));
- //convert half-open to inclusive ranges for use with wxDC::DrawRectangle
- if (pixelFrom.x != pixelTo.x) //no matter how small the selection, always draw at least one pixel!
- {
- pixelFrom.x -= pixelFrom.x < pixelTo.x ? 0 : 1;
- pixelTo .x -= pixelFrom.x < pixelTo.x ? 1 : 0;
- }
- if (pixelFrom.y != pixelTo.y)
- {
- pixelFrom.y -= pixelFrom.y < pixelTo.y ? 0 : 1;
- pixelTo .y -= pixelFrom.y < pixelTo.y ? 1 : 0;
- }
- confine(pixelFrom.x, 0, graphArea.width - 1);
- confine(pixelTo .x, 0, graphArea.width - 1);
- confine(pixelFrom.y, 0, graphArea.height - 1);
- confine(pixelTo .y, 0, graphArea.height - 1);
-
- pixelFrom += graphAreaOrigin;
- pixelTo += graphAreaOrigin;
+ if (*low > *high)
+ std::swap(low, high);
+ *low += 0.5;
+ *high -= 0.5;
+ if (*low > *high)
+ *low = *high = (*low + *high) / 2;
+ };
+ for (auto it = allSelections.begin(); it != allSelections.end(); ++it)
+ {
+ //harmonize with active mouse selection above
+ double screenFromX = cvrtX.realToScreen(it->from.x);
+ double screenFromY = cvrtY.realToScreen(it->from.y);
+ double screenToX = cvrtX.realToScreen(it->to.x);
+ double screenToY = cvrtY.realToScreen(it->to.y);
+ shrink(&screenFromX, &screenToX);
+ shrink(&screenFromY, &screenToY);
+
+ confine(screenFromX, 0.0, graphArea.width - 1.0);
+ confine(screenFromY, 0.0, graphArea.height - 1.0);
+ confine(screenToX, 0.0, graphArea.width - 1.0);
+ confine(screenToY, 0.0, graphArea.height - 1.0);
+
+ const wxPoint pixelFrom = wxPoint(numeric::round(screenFromX),
+ numeric::round(screenFromY)) + graphAreaOrigin;
+ const wxPoint pixelTo = wxPoint(numeric::round(screenToX),
+ numeric::round(screenToY)) + graphAreaOrigin;
switch (attr.mouseSelMode)
{
case SELECT_NONE:
@@ -554,17 +754,42 @@ void Graph2D::render(wxDC& dc) const
drawYLabel(dc, minY, maxY, blockCountY, cvrtY, graphArea, yLabelArea, *attr.labelFmtY);
//4. finally draw curves
- for (auto it = curves_.begin(); it != curves_.end(); ++it)
{
- std::vector<wxPoint> points;
- getCurvePoints(it - curves_.begin(), points);
- if (!points.empty())
+ dc.SetClippingRegion(graphArea); //prevent thick curves from drawing slightly outside
+ ZEN_ON_SCOPE_EXIT(dc.DestroyClippingRegion());
+
+ for (auto it = curves_.begin(); it != curves_.end(); ++it)
{
wxDCPenChanger dummy(dc, wxPen(it->second.color, it->second.lineWidth));
- dc.DrawLines(static_cast<int>(points.size()), &points[0]);
- dc.DrawPoint(points.back()); //wxDC::DrawLines() doesn't draw last pixel
+
+ const size_t index = it - curves_.begin();
+ std::vector<wxPoint>& points = drawPoints[index]; //alas wxDC::DrawLines() is not const-correct!!!
+ auto& marker = oobMarker [index];
+ assert(points.size() == marker.size());
+ warn_static("review")
+
+ //draw all parts of the curve except for the out-of-bounds ranges
+ size_t pointsIndexFirst = 0;
+ while (pointsIndexFirst < points.size())
+ {
+ size_t pointsIndexLast = std::find(marker.begin() + pointsIndexFirst, marker.end(), true) - marker.begin();
+ if (pointsIndexLast < points.size()) ++ pointsIndexLast;
+
+ const int pointCount = static_cast<int>(pointsIndexLast - pointsIndexFirst);
+ if (pointCount > 0)
+ {
+ if (pointCount >= 2) //on OS X wxWidgets has a nasty assert on this
+ dc.DrawLines(pointCount, &points[pointsIndexFirst]);
+ dc.DrawPoint(points[pointsIndexLast - 1]); //wxDC::DrawLines() doesn't draw last pixel
+ }
+ pointsIndexFirst = std::find(marker.begin() + pointsIndexLast, marker.end(), false) - marker.begin();
+ }
}
}
+
+ //5. draw corner texts
+ for (auto it = attr.cornerTexts.begin(); it != attr.cornerTexts.end(); ++it)
+ drawCornerText(dc, graphArea, it->second, it->first);
}
}
}
bgstack15