// ************************************************************************** // * This file is part of the FreeFileSync project. It is distributed under * // * GNU General Public License: http://www.gnu.org/licenses/gpl.html * // * Copyright (C) Zenju (zenju AT gmx DOT de) - All Rights Reserved * // ************************************************************************** #include "graph.h" #include #include #include #include #include #include #include "dc.h" using namespace zen; //todo: support zoom via mouse wheel const wxEventType zen::wxEVT_GRAPH_SELECTION = wxNewEventType(); const std::shared_ptr Graph2D::MainAttributes::defaultFormat = std::make_shared(); //for some buggy reason MSVC isn't able to use a temporary as a default argument double zen::nextNiceNumber(double blockSize) //round to next number which is a convenient to read block size { if (blockSize <= 0) return 0; const double k = std::floor(std::log10(blockSize)); 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); //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 }; return e * numeric::nearMatch(a, std::begin(steps), std::end(steps)); } namespace { wxColor getDefaultColor(size_t pos) { switch (pos % 10) { case 0: return wxColor(0, 69, 134); //blue case 1: return wxColor(255, 66, 14); //red case 2: return wxColor(255, 211, 32); //yellow case 3: return wxColor(87, 157, 28); //green case 4: return wxColor(126, 0, 33); //royal case 5: return wxColor(131, 202, 255); //light blue case 6: return wxColor(49, 64, 4); //dark green case 7: return wxColor(174, 207, 0); //light green case 8: return wxColor(75, 31, 111); //purple case 9: return wxColor(255, 149, 14); //orange } assert(false); return *wxBLACK; } class ConvertCoord //convert between screen and input data coordinates { 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)), 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_; } double realToScreen(double realPos) const //return screen position in pixel (but with double precision!) { return (realPos - min_) * scaleToScr; } 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)); } 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 optimalBlockSizePx, // const LabelFormatter& labelFmt) { if (graphAreaSize > 0) { double valRangePerBlock = (valMax - valMin) * optimalBlockSizePx / graphAreaSize; //proposal valRangePerBlock = labelFmt.getOptimalBlockSize(valRangePerBlock); if (!numeric::isNull(valRangePerBlock)) { valMin = std::floor(valMin / valRangePerBlock) * valRangePerBlock; valMax = std::ceil (valMax / valRangePerBlock) * valRangePerBlock; blockCount = numeric::round((valMax - valMin) / valRangePerBlock); //"round" to avoid IEEE 754 surprises return; } } blockCount = 0; } void drawXLabel(wxDC& dc, double xMin, double xMax, int blockCount, const ConvertCoord& cvrtX, const wxRect& graphArea, const wxRect& labelArea, const LabelFormatter& labelFmt) { assert(graphArea.width == labelArea.width && graphArea.x == labelArea.x); if (blockCount <= 0) return; wxDCPenChanger dummy(dc, wxPen(wxColor(192, 192, 192))); //light grey wxDCTextColourChanger dummy2(dc, wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOWTEXT)); //use user setting for labels const double valRangePerBlock = (xMax - xMin) / blockCount; for (int i = 1; i < blockCount; ++i) { //draw grey vertical lines const double valX = xMin + i * valRangePerBlock; //step over raw data, not graph area pixels, to not lose precision const int x = graphArea.x + cvrtX.realToScreenRound(valX); if (graphArea.height > 0) dc.DrawLine(wxPoint(x, graphArea.y), wxPoint(x, graphArea.y + graphArea.height)); //wxDC::DrawLine() doesn't draw last pixel //draw x axis labels const wxString label = labelFmt.formatText(valX, valRangePerBlock); const wxSize labelExtent = dc.GetMultiLineTextExtent(label); dc.DrawText(label, wxPoint(x - labelExtent.GetWidth() / 2, labelArea.y + (labelArea.height - labelExtent.GetHeight()) / 2)); //center } } void drawYLabel(wxDC& dc, double yMin, double yMax, int blockCount, const ConvertCoord& cvrtY, const wxRect& graphArea, const wxRect& labelArea, const LabelFormatter& labelFmt) { assert(graphArea.height == labelArea.height && graphArea.y == labelArea.y); if (blockCount <= 0) return; wxDCPenChanger dummy(dc, wxPen(wxColor(192, 192, 192))); //light grey wxDCTextColourChanger dummy2(dc, wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOWTEXT)); //use user setting for labels const double valRangePerBlock = (yMax - yMin) / blockCount; for (int i = 1; i < blockCount; ++i) { //draw grey horizontal lines const double valY = yMin + i * valRangePerBlock; //step over raw data, not graph area pixels, to not lose precision const int y = graphArea.y + cvrtY.realToScreenRound(valY); if (graphArea.width > 0) dc.DrawLine(wxPoint(graphArea.x, y), wxPoint(graphArea.x + graphArea.width, y)); //wxDC::DrawLine() doesn't draw last pixel //draw y axis labels const wxString label = labelFmt.formatText(valY, valRangePerBlock); const wxSize labelExtent = dc.GetMultiLineTextExtent(label); dc.DrawText(label, wxPoint(labelArea.x + (labelArea.width - labelExtent.GetWidth()) / 2, y - labelExtent.GetHeight() / 2)); //center } } void drawCornerText(wxDC& dc, const wxRect& graphArea, const wxString& txt, Graph2D::PosCorner pos) { 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"? 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; 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 void cutPoints(std::vector& curvePoints, std::vector& 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 curvePointsTmp; std::vector 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& curvePoints, std::vector& 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& curvePoints, std::vector& 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& points) const { if (pixelWidth <= 1) return; const ConvertCoord cvrtX(minX, maxX, pixelWidth - 1); //map [minX, maxX] to [0, pixelWidth - 1] std::pair 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& points) const { if (pixelWidth <= 1) return; const ConvertCoord cvrtX(minX, maxX, pixelWidth - 1); //map [minX, maxX] to [0, pixelWidth - 1] const std::pair 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 ptLe = getLessEq(x); Opt 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), 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); //http://wiki.wxwidgets.org/Flicker-Free_Drawing Connect(wxEVT_ERASE_BACKGROUND, wxEraseEventHandler(Graph2D::onEraseBackGround), nullptr, this); //SetDoubleBuffered(true); slow as hell! #if wxCHECK_VERSION(2, 9, 1) SetBackgroundStyle(wxBG_STYLE_PAINT); #else SetBackgroundStyle(wxBG_STYLE_CUSTOM); #endif Connect(wxEVT_LEFT_DOWN, wxMouseEventHandler(Graph2D::OnMouseLeftDown), nullptr, this); Connect(wxEVT_MOTION, wxMouseEventHandler(Graph2D::OnMouseMovement), nullptr, this); Connect(wxEVT_LEFT_UP, wxMouseEventHandler(Graph2D::OnMouseLeftUp), nullptr, this); Connect(wxEVT_MOUSE_CAPTURE_LOST, wxMouseCaptureLostEventHandler(Graph2D::OnMouseCaptureLost), nullptr, this); } void Graph2D::onPaintEvent(wxPaintEvent& event) { //wxAutoBufferedPaintDC dc(this); -> this one happily fucks up for RTL layout by not drawing the first column (x = 0)! BufferedPaintDC dc(*this, doubleBuffer); render(dc); } void Graph2D::OnMouseLeftDown(wxMouseEvent& event) { activeSel.reset(new MouseSelection(*this, event.GetPosition())); if (!event.ControlDown()) oldSel.clear(); Refresh(); } void Graph2D::OnMouseMovement(wxMouseEvent& event) { if (activeSel.get()) { activeSel->refCurrentPos() = event.GetPosition(); //corresponding activeSel->refSelection() is updated in Graph2D::render() Refresh(); } } void Graph2D::OnMouseLeftUp(wxMouseEvent& event) { if (activeSel.get()) { if (activeSel->getStartPos() != activeSel->refCurrentPos()) //if it's just a single mouse click: discard selection { GraphSelectEvent selEvent(activeSel->refSelection()); //fire off GraphSelectEvent if (wxEvtHandler* handler = GetEventHandler()) handler->AddPendingEvent(selEvent); oldSel.push_back(activeSel->refSelection()); //commit selection } activeSel.reset(); Refresh(); } } void Graph2D::OnMouseCaptureLost(wxMouseCaptureLostEvent& event) { activeSel.reset(); Refresh(); } void Graph2D::setCurve(const std::shared_ptr& data, const CurveAttributes& ca) { curves_.clear(); addCurve(data, ca); } void Graph2D::addCurve(const std::shared_ptr& data, const CurveAttributes& ca) { CurveAttributes newAttr = ca; if (newAttr.autoColor) newAttr.setColor(getDefaultColor(curves_.size())); curves_.push_back(std::make_pair(data, newAttr)); Refresh(); } 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(labelFont); const wxRect clientRect = GetClientRect(); //DON'T use wxDC::GetSize()! DC may be larger than visible area! { //clear complete client area; set label background color const wxColor backCol = GetBackgroundColour(); //user-configurable! //wxPanel::GetClassDefaultAttributes().colBg : //wxSystemSettings::GetColour(wxSYS_COLOUR_BTNFACE); wxDCPenChanger dummy (dc, backCol); wxDCBrushChanger dummy2(dc, backCol); dc.DrawRectangle(clientRect); } /* ----------------------- | | x-label | ----------------------- |y-label | graph area | |---------------------- */ wxRect graphArea = clientRect; int xLabelPosY = clientRect.y; int yLabelPosX = clientRect.x; switch (attr.labelposX) { case X_LABEL_TOP: graphArea.y += attr.xLabelHeight; graphArea.height -= attr.xLabelHeight; break; case X_LABEL_BOTTOM: xLabelPosY += clientRect.height - attr.xLabelHeight; graphArea.height -= attr.xLabelHeight; break; case X_LABEL_NONE: break; } switch (attr.labelposY) { case Y_LABEL_LEFT: graphArea.x += attr.yLabelWidth; graphArea.width -= attr.yLabelWidth; break; case Y_LABEL_RIGHT: yLabelPosX += clientRect.width - attr.yLabelWidth; graphArea.width -= attr.yLabelWidth; break; case Y_LABEL_NONE: break; } { //paint graph background (excluding label area) wxDCPenChanger dummy (dc, wxColour(130, 135, 144)); //medium grey, the same Win7 uses for other frame borders wxDCBrushChanger dummy2(dc, *wxWHITE); //accessibility: we have to set both back- and foreground colors or none at all! dc.DrawRectangle(graphArea); graphArea.Deflate(1, 1); //attention more wxWidgets design mistakes: behavior of wxRect::Deflate depends on object being const/non-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::infinity() : attr.minX; //automatic: ensure values are initialized by first curve double maxX = attr.maxXauto ? -std::numeric_limits::infinity() : attr.maxX; // for (auto it = curves_.begin(); it != curves_.end(); ++it) if (const CurveData* curve = it->first.get()) { const std::pair 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::infinity()) //valid x-range { int blockCountX = 0; //enlarge minX, maxX to a multiple of a "useful" block size if (attr.labelposX != X_LABEL_NONE && attr.labelFmtX.get()) widenRange(minX, maxX, //in/out blockCountX, //out graphArea.width, dc.GetTextExtent(L"100000000000000").GetWidth(), *attr.labelFmtX); //get raw values + detect y value range double minY = attr.minYauto ? std::numeric_limits::infinity() : attr.minY; //automatic: ensure values are initialized by first curve double maxY = attr.maxYauto ? -std::numeric_limits::infinity() : attr.maxY; // std::vector> curvePoints(curves_.size()); std::vector> oobMarker (curves_.size()); //effectively a std::vector marking points that start a out of bounds line 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& points = curvePoints[index]; auto& marker = oobMarker[index]; 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 { int blockCountY = 0; //enlarge minY, maxY to a multiple of a "useful" block size if (attr.labelposY != Y_LABEL_NONE && attr.labelFmtY.get()) widenRange(minY, maxY, //in/out blockCountY, //out graphArea.height, 3 * dc.GetTextExtent(L"1").GetHeight(), *attr.labelFmtY); 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 std::vector> drawPoints(curves_.size()); 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) { auto widen = [](double* low, double* high) { if (*low > *high) std::swap(low, high); *low -= 0.5; *high += 0.5; }; const wxPoint screenStart = activeSel->getStartPos() - graphAreaOrigin; //make relative to graphArea const wxPoint screenCurrent = activeSel->refCurrentPos() - graphAreaOrigin; //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 #################### //1. draw colored area under curves for (auto it = curves_.begin(); it != curves_.end(); ++it) if (it->second.drawCurveArea) { std::vector points = drawPoints[it - curves_.begin()]; if (!points.empty()) { points.push_back(wxPoint(points.back ().x, graphArea.GetBottom())); //add lower right and left corners points.push_back(wxPoint(points.front().x, graphArea.GetBottom())); // wxDCBrushChanger dummy(dc, it->second.fillColor); wxDCPenChanger dummy2(dc, it->second.fillColor); dc.DrawPolygon(static_cast(points.size()), &points[0]); } } //2. draw all currently set mouse selections (including active selection) std::vector allSelections = oldSel; if (activeSel) allSelections.push_back(activeSel->refSelection()); { //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 auto shrink = [](double* low, double* high) { 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: break; case SELECT_RECTANGLE: dc.DrawRectangle(wxRect(pixelFrom, pixelTo)); break; case SELECT_X_AXIS: dc.DrawRectangle(wxRect(wxPoint(pixelFrom.x, graphArea.y), wxPoint(pixelTo.x, graphArea.y + graphArea.height - 1))); break; case SELECT_Y_AXIS: dc.DrawRectangle(wxRect(wxPoint(graphArea.x, pixelFrom.y), wxPoint(graphArea.x + graphArea.width - 1, pixelTo.y))); break; } } } //3. draw labels and background grid drawXLabel(dc, minX, maxX, blockCountX, cvrtX, graphArea, xLabelArea, *attr.labelFmtX); drawYLabel(dc, minY, maxY, blockCountY, cvrtY, graphArea, yLabelArea, *attr.labelFmtY); //4. finally draw curves { 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)); const size_t index = it - curves_.begin(); std::vector& 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(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); } } }