From 5712143819f9b03d703f94d427b568c5419ad2ae Mon Sep 17 00:00:00 2001 From: Christina Morency Date: Wed, 18 Mar 2026 16:05:42 -0700 Subject: [PATCH] Add 1D line plot view --- src/CMakeLists.txt | 1 + src/ncvis.cpp | 4 + src/wxImagePanel.cpp | 31 ++++ src/wxImagePanel.h | 5 + src/wxNcVisFrame.cpp | 298 +++++++++++++++++++++++++++++++++++++++ src/wxNcVisFrame.h | 27 ++++ src/wxNcVisLinePlot.cpp | 304 ++++++++++++++++++++++++++++++++++++++++ src/wxNcVisLinePlot.h | 72 ++++++++++ 8 files changed, 742 insertions(+) create mode 100644 src/wxNcVisLinePlot.cpp create mode 100644 src/wxNcVisLinePlot.h diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 075c56d..40d4b50 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -7,6 +7,7 @@ set(NCVIS_SOURCE_FILES wxNcVisFrame.cpp wxNcVisOptionsDialog.cpp wxNcVisExportDialog.cpp + wxNcVisLinePlot.cpp wxImagePanel.cpp GridDataSampler.cpp ColorMap.cpp diff --git a/src/ncvis.cpp b/src/ncvis.cpp index 5fac5f1..e5a4c31 100644 --- a/src/ncvis.cpp +++ b/src/ncvis.cpp @@ -43,6 +43,10 @@ wxIMPLEMENT_APP(wxNcVisApp); bool wxNcVisApp::OnInit() { + // PNG handler + wxInitAllImageHandlers(); + wxImage::AddHandler(new wxPNGHandler); + // Turn off fatal errors in NetCDF NcError error(NcError::silent_nonfatal); diff --git a/src/wxImagePanel.cpp b/src/wxImagePanel.cpp index cfdf89b..9fb89de 100644 --- a/src/wxImagePanel.cpp +++ b/src/wxImagePanel.cpp @@ -23,6 +23,7 @@ EVT_PAINT(wxImagePanel::OnPaint) EVT_SIZE(wxImagePanel::OnSize) EVT_IDLE(wxImagePanel::OnIdle) EVT_LEFT_DCLICK(wxImagePanel::OnMouseLeftDoubleClick) +EVT_LEFT_DOWN(wxImagePanel::OnMouseLeftDown) EVT_MOTION(wxImagePanel::OnMouseMotion) EVT_LEAVE_WINDOW(wxImagePanel::OnMouseLeaveWindow) END_EVENT_TABLE() @@ -442,6 +443,36 @@ void wxImagePanel::OnMouseLeftDoubleClick(wxMouseEvent & evt) { //////////////////////////////////////////////////////////////////////////////// +void wxImagePanel::OnMouseLeftDown(wxMouseEvent & evt) { + + if (!evt.ShiftDown()) { + evt.Skip(); + return; + } + + wxPoint pos = evt.GetPosition(); + + wxSize wxsMap; + wxPosition wxpMap; + GetMapPositionSize(wxsMap, wxpMap); + + pos.x -= wxpMap.GetCol(); + pos.y -= wxpMap.GetRow(); + + if ((pos.x < 0) || (pos.x >= (int)m_dSampleX.size())) return; + if ((pos.y < 0) || (pos.y >= (int)m_dSampleY.size())) return; + + const double dLon = m_dSampleX[pos.x]; + const double dLat = m_dSampleY[m_dSampleY.size() - pos.y - 1]; + + _ASSERT(m_pncvisparent != NULL); + + // Open/update the time series plot + m_pncvisparent->OnShiftClickTimeSeries(dLat, dLon); +} + +//////////////////////////////////////////////////////////////////////////////// + void wxImagePanel::FormatLabelBarLabel( double dValue, std::string & str diff --git a/src/wxImagePanel.h b/src/wxImagePanel.h index ec63847..1bd36a7 100644 --- a/src/wxImagePanel.h +++ b/src/wxImagePanel.h @@ -91,6 +91,11 @@ class wxImagePanel : public wxPanel { /// void OnMouseLeftDoubleClick(wxMouseEvent & evt); + /// + /// Callback for when Shift + mouse left-clicked. + /// + void OnMouseLeftDown(wxMouseEvent & evt); + public: /// /// Format a label bar label from a value. diff --git a/src/wxNcVisFrame.cpp b/src/wxNcVisFrame.cpp index a9ede95..26b408b 100644 --- a/src/wxNcVisFrame.cpp +++ b/src/wxNcVisFrame.cpp @@ -11,11 +11,17 @@ #include "wxNcVisOptionsDialog.h" #include "wxNcVisExportDialog.h" +#include "wxNcVisLinePlot.h" #include "STLStringHelper.h" #include "ShpFile.h" #include "TimeObj.h" #include +#include +#include #include +#include +#include +#include //////////////////////////////////////////////////////////////////////////////// @@ -96,6 +102,7 @@ wxNcVisFrame::wxNcVisFrame( m_rightsizer(NULL), m_vardimsizer(NULL), m_imagepanel(NULL), + m_pLinePlot(NULL), m_wxNcVisExportDialog(NULL), m_wxDimTimer(this,ID_DIMTIMER), m_varActive(NULL), @@ -1084,6 +1091,297 @@ void wxNcVisFrame::MapSampleCoords1DFromActiveVar( //////////////////////////////////////////////////////////////////////////////// +bool wxNcVisFrame::GetTimeDimensionForActiveVar(long & lTimeDim) const { + + lTimeDim = -1; + if (m_varActive == NULL) { + return false; + } + + const int nDims = m_varActive->num_dims(); + if (nDims <= 0) { + return false; + } + + for (int d = 0; d < nDims; ++d) { + NcDim * pDim = m_varActive->get_dim(d); + if (pDim == NULL) continue; + + std::string name = pDim->name(); + std::transform(name.begin(), name.end(), name.begin(), ::tolower); + + if ((name == "time") || + (name == "times") || + (name == "t") || + (name.find("time") != std::string::npos)) + { + lTimeDim = d; + return true; + } + } + + // Fallback assumption (common): first dimension is time + lTimeDim = 0; + return true; +} + +//////////////////////////////////////////////////////////////////////////////// + +bool wxNcVisFrame::BuildTimeSeriesAtLatLon( + double dLat, double dLon, + std::vector & vecTime, + std::vector & vecValue, + double & dLatNearest, + double & dLonNearest, + wxString & outTimeUnits, + time_t & outBaseEpoch +) { + vecTime.clear(); + vecValue.clear(); + dLatNearest = dLat; + dLonNearest = dLon; + + if (m_varActive == NULL) return false; + + // ------------------------------------------------------------ + // Find which NcFile contains the active variable (ncvis style) + // ------------------------------------------------------------ + NcFile * pFile = NULL; + const std::string strVarName = m_varActive->name(); + + for (int vc = 0; vc < NcVarMaximumDimensions; ++vc) { + auto itVar = m_mapVarNames[vc].find(strVarName); + if (itVar != m_mapVarNames[vc].end()) { + const long ixFile = itVar->second[0]; + if ((ixFile < 0) || (ixFile >= (long)m_vecpncfiles.size())) return false; + pFile = m_vecpncfiles[ixFile]; + break; + } + } + if (pFile == NULL) return false; + + // ------------------------------------------------------------ + // Identify time / lat / lon dimensions of the active variable + // ------------------------------------------------------------ + long lTimeDim = -1; + if (!GetTimeDimensionForActiveVar(lTimeDim)) return false; + + const int nDims = m_varActive->num_dims(); + if (nDims < 3) return false; // expecting (time,lat,lon) style + + long lLatDim = -1; + long lLonDim = -1; + + for (int d = 0; d < nDims; ++d) { + NcDim * pDim = m_varActive->get_dim(d); + if (pDim == NULL) continue; + + std::string name = pDim->name(); + std::transform(name.begin(), name.end(), name.begin(), ::tolower); + + if (name == "lat" || name == "latitude" || name.find("lat") != std::string::npos) { + lLatDim = d; + } + if (name == "lon" || name == "longitude" || name.find("lon") != std::string::npos) { + lLonDim = d; + } + } + + // Fallback for typical ordering if names weren't found + if (lLatDim < 0 || lLonDim < 0) { + if (lTimeDim == 0 && nDims >= 3) { + lLatDim = 1; + lLonDim = 2; + } else { + return false; + } + } + + const long nT = m_varActive->get_dim((int)lTimeDim)->size(); + const long nLat = m_varActive->get_dim((int)lLatDim)->size(); + const long nLon = m_varActive->get_dim((int)lLonDim)->size(); + + if (nT <= 0 || nLat <= 0 || nLon <= 0) return false; + + // ------------------------------------------------------------ + // Read 1D coordinate variables for lat/lon from this file + // ------------------------------------------------------------ + NcVar * varLat = pFile->get_var("lat"); + if (varLat == NULL) varLat = pFile->get_var("latitude"); + if (varLat == NULL) varLat = pFile->get_var(m_varActive->get_dim((int)lLatDim)->name()); + + NcVar * varLon = pFile->get_var("lon"); + if (varLon == NULL) varLon = pFile->get_var("longitude"); + if (varLon == NULL) varLon = pFile->get_var(m_varActive->get_dim((int)lLonDim)->name()); + + if (varLat == NULL || varLon == NULL) return false; + + std::vector lat(nLat), lon(nLon); + if (!varLat->get(&lat[0], (long)nLat)) return false; + if (!varLon->get(&lon[0], (long)nLon)) return false; + + auto nearestIndex = [](const std::vector & a, double x) -> long { + long best = 0; + double bestd = std::numeric_limits::infinity(); + for (long i = 0; i < (long)a.size(); ++i) { + double d = std::fabs(a[i] - x); + if (d < bestd) { bestd = d; best = i; } + } + return best; + }; + + const long iLat = nearestIndex(lat, dLat); + const long iLon = nearestIndex(lon, dLon); + + dLatNearest = lat[iLat]; + dLonNearest = lon[iLon]; + + // ------------------------------------------------------------ + // Time coordinate: try "time" variable + // ------------------------------------------------------------ + vecTime.resize(nT); + + for (long it = 0; it < nT; ++it) { + vecTime[it] = (double)it; + } + + NcVar * varTime = pFile->get_var("time"); + + outTimeUnits = "time"; + outBaseEpoch = 0; + + if (varTime != NULL) { + + // ---- Read time coordinate values robustly (works with netcdfcpp.h types) ---- + NcValues * pTimeVals = varTime->values(); + if (pTimeVals != NULL) { + long n = pTimeVals->num(); + if (n > nT) n = nT; + for (long it = 0; it < n; ++it) { + vecTime[it] = pTimeVals->as_double(it); + } + delete pTimeVals; + } + + // ---- Read units attribute and parse base date ---- + NcAtt * attUnits = varTime->get_att("units"); + if (attUnits != NULL) { + char buf[256]; + buf[0] = '\0'; + + NcValues * pVals = attUnits->values(); + if (pVals != NULL) { + const char * psz = pVals->as_string(0); + if (psz != NULL) { + std::strncpy(buf, psz, sizeof(buf)-1); + buf[sizeof(buf)-1] = '\0'; + } + delete pVals; + } + delete attUnits; + + outTimeUnits = wxString::Format("time (%s)", buf); + + // Parse "days since YYYY-MM-DD HH:MM:SS" (time optional) + std::string s(buf); + size_t p = s.find("since"); + if (p != std::string::npos) { + std::string rest = s.substr(p + 5); + while (!rest.empty() && (rest[0] == ' ' || rest[0] == '\t')) rest.erase(rest.begin()); + + int Y=0,M=0,D=0,h=0,m=0,sec=0; + int nn = std::sscanf(rest.c_str(), "%d-%d-%d %d:%d:%d", &Y,&M,&D,&h,&m,&sec); + if (nn >= 3) { + std::tm tmv; + std::memset(&tmv, 0, sizeof(tmv)); + tmv.tm_year = Y - 1900; + tmv.tm_mon = M - 1; + tmv.tm_mday = D; + tmv.tm_hour = (nn >= 4) ? h : 0; + tmv.tm_min = (nn >= 5) ? m : 0; + tmv.tm_sec = (nn >= 6) ? sec : 0; + + outBaseEpoch = std::mktime(&tmv); + } + } + } + } + + // ------------------------------------------------------------ + // Extract scalar value at (time,it) & fixed (lat,lon) + // ------------------------------------------------------------ + vecValue.resize(nT); + + std::vector cur(nDims, 0); + cur[(int)lLatDim] = iLat; + cur[(int)lLonDim] = iLon; + + for (long it = 0; it < nT; ++it) { + cur[(int)lTimeDim] = it; + + bool ok = false; + if (nDims == 3) { + ok = m_varActive->set_cur(cur[0], cur[1], cur[2]); + } else if (nDims == 4) { + ok = m_varActive->set_cur(cur[0], cur[1], cur[2], cur[3]); + } else if (nDims == 5) { + ok = m_varActive->set_cur(cur[0], cur[1], cur[2], cur[3], cur[4]); + } else { + return false; + } + if (!ok) return false; + + float val = std::numeric_limits::quiet_NaN(); + + if (nDims == 3) { + if (!m_varActive->get(&val, 1, 1, 1)) return false; + } else if (nDims == 4) { + if (!m_varActive->get(&val, 1, 1, 1, 1)) return false; + } else if (nDims == 5) { + if (!m_varActive->get(&val, 1, 1, 1, 1, 1)) return false; + } + + vecValue[it] = val; + } + + return true; +} + +//////////////////////////////////////////////////////////////////////////////// + +void wxNcVisFrame::OnShiftClickTimeSeries(double dLat, double dLon) { + + std::vector vecTime; + std::vector vecValue; + double dLatNearest = dLat; + double dLonNearest = dLon; + + wxString timeUnits("time"); + time_t baseEpoch = 0; + + if (!BuildTimeSeriesAtLatLon(dLat, dLon, vecTime, vecValue, dLatNearest, dLonNearest, timeUnits, baseEpoch)) { + SetStatusMessage("Shift+click: failed to extract time series", false); + return; + } + + wxString title = wxString::Format( + "%s time series (lat,lon)=(%.4f, %.4f)", + GetVarActiveTitle().c_str(), dLatNearest, dLonNearest); + + wxString yLabel = wxString::Format("%s (%s)", + GetVarActiveTitle().c_str(), GetVarActiveUnits().c_str()); + + if (m_pLinePlot == NULL) { + m_pLinePlot = new wxNcVisLinePlotFrame(this, title, vecTime, vecValue, timeUnits, baseEpoch, yLabel); + m_pLinePlot->Show(); + } else { + m_pLinePlot->UpdatePlot(title, vecTime, vecValue, timeUnits, baseEpoch, yLabel); + if (!m_pLinePlot->IsShown()) m_pLinePlot->Show(); + } +} + +//////////////////////////////////////////////////////////////////////////////// + void wxNcVisFrame::SampleData( const std::vector & dSampleX, const std::vector & dSampleY, diff --git a/src/wxNcVisFrame.h b/src/wxNcVisFrame.h index ac85ded..13fb6bf 100644 --- a/src/wxNcVisFrame.h +++ b/src/wxNcVisFrame.h @@ -20,11 +20,13 @@ #include #include +#include //////////////////////////////////////////////////////////////////////////////// class wxNcVisOptionsDialog; class wxNcVisExportDialog; +class wxNcVisLinePlotFrame; //////////////////////////////////////////////////////////////////////////////// @@ -317,6 +319,11 @@ class wxNcVisFrame : public wxFrame { return m_strVarActiveUnits; } + /// + /// Callback triggered when Shift+mouse left click for 1D plot + /// + void OnShiftClickTimeSeries(double dLat, double dLon); + private: /// /// Callback triggered when Exit is selected in the menu. @@ -514,6 +521,11 @@ class wxNcVisFrame : public wxFrame { /// wxTimer m_wxDimTimer; + /// + /// 1D line plot. + /// + wxNcVisLinePlotFrame * m_pLinePlot; + private: /// /// Flag indicating verbose output is desired. @@ -676,6 +688,21 @@ class wxNcVisFrame : public wxFrame { /// VariableNameFileIxMap m_mapVarNames[10]; + /// + /// Extracting 1D series for 1D plot. + /// + bool BuildTimeSeriesAtLatLon( + double dLat, double dLon, + std::vector & vecTime, + std::vector & vecValue, + double & dLatNearest, + double & dLonNearest, + wxString & outTimeUnits, + time_t & outBaseEpoch + ); + + bool GetTimeDimensionForActiveVar(long & lTimeDim) const; + private: /// /// ColorMap index. diff --git a/src/wxNcVisLinePlot.cpp b/src/wxNcVisLinePlot.cpp new file mode 100644 index 0000000..2026d73 --- /dev/null +++ b/src/wxNcVisLinePlot.cpp @@ -0,0 +1,304 @@ +#include "wxNcVisLinePlot.h" +#include +#include +#include +#include +#include + +enum { + ID_LinePlot_ExportPng = wxID_HIGHEST + 1 +}; + +wxBEGIN_EVENT_TABLE(wxNcVisLinePlotPanel, wxPanel) + EVT_PAINT(wxNcVisLinePlotPanel::OnPaint) +wxEND_EVENT_TABLE() + +wxNcVisLinePlotPanel::wxNcVisLinePlotPanel( + wxWindow * parent, + const std::vector & x, + const std::vector & y, + const wxString & xLabel, + time_t baseEpoch, + const wxString & yLabel, + const wxString & title +) : wxPanel(parent), + m_x(x), m_y(y), + m_xLabel(xLabel), + m_baseEpoch(baseEpoch), + m_yLabel(yLabel), + m_title(title) +{ + SetBackgroundStyle(wxBG_STYLE_PAINT); +} + +wxString wxNcVisLinePlotPanel::FormatXValue(double xv) const { + if (m_baseEpoch == 0) { + return wxString::Format("%.4g", xv); + } + + time_t t = m_baseEpoch + static_cast(std::llround(xv * 86400.0)); + wxDateTime dt(t); + + if (!dt.IsValid()) { + return wxString::Format("%.4g", xv); + } + + return dt.Format("%Y-%m-%d"); +} + +void wxNcVisLinePlotPanel::SetData( + const std::vector & x, + const std::vector & y, + const wxString & xLabel, + time_t baseEpoch, + const wxString & yLabel, + const wxString & title +) { + m_x = x; + m_y = y; + m_xLabel = xLabel; + m_baseEpoch = baseEpoch; + m_yLabel = yLabel; + m_title = title; + Refresh(); +} + +void wxNcVisLinePlotPanel::OnPaint(wxPaintEvent &) { + wxAutoBufferedPaintDC dc(this); + DrawPlot(dc, GetClientSize()); +} + +void wxNcVisLinePlotPanel::DrawPlot(wxDC & dc, const wxSize & sz) { + dc.SetBackground(*wxWHITE_BRUSH); + dc.Clear(); + + if (sz.x < 80 || sz.y < 80) return; + if (m_x.size() < 2 || m_y.size() < 2) return; + + // Padding tuned for titles + range labels + const int padL = 85, padR = 60, padT = 35, padB = 85; + const int w = sz.x - padL - padR; + const int h = sz.y - padT - padB; + if (w <= 10 || h <= 10) return; + + // Plot rectangle (white box) + wxRect plotRect(padL, padT, w, h); + dc.SetBrush(*wxWHITE_BRUSH); + dc.SetPen(*wxBLACK_PEN); + dc.DrawRectangle(plotRect); + + // Bounds + auto mm = std::minmax_element(m_x.begin(), m_x.end()); + double xmin = *(mm.first); + double xmax = *(mm.second); + + float ymin = m_y[0], ymax = m_y[0]; + for (float v : m_y) { + ymin = std::min(ymin, v); + ymax = std::max(ymax, v); + } + + // Avoid zero ranges + if (xmax == xmin) xmax = xmin + 1.0; + if (ymax == ymin) ymax = ymin + 1.0f; + + auto X = [&](double xv) -> int { + return padL + (int)std::lround((xv - xmin) * (double)w / (xmax - xmin)); + }; + auto Y = [&](float yv) -> int { + return padT + (int)std::lround((double)(ymax - yv) * (double)h / (double)(ymax - ymin)); + }; + + const int nXTicks = 6; + const int nYTicks = 6; + + // Grid lines + dc.SetPen(wxPen(wxColour(180,180,180), 1, wxPENSTYLE_DOT)); + + for (int i = 0; i < nXTicks; ++i) { + double xv = xmin + (xmax - xmin) * static_cast(i) / static_cast(nXTicks - 1); + int px = X(xv); + dc.DrawLine(px, padT, px, padT + h); + } + + for (int i = 0; i < nYTicks; ++i) { + double yv = ymin + (ymax - ymin) * static_cast(i) / static_cast(nYTicks - 1); + int py = Y(static_cast(yv)); + dc.DrawLine(padL, py, padL + w, py); + } + + // Redraw border on top of grid + dc.SetPen(*wxBLACK_PEN); + dc.SetBrush(*wxTRANSPARENT_BRUSH); + dc.DrawRectangle(plotRect); + + // Title (top) + wxFont fontTitle = dc.GetFont(); + fontTitle.SetWeight(wxFONTWEIGHT_BOLD); + fontTitle.SetPointSize(fontTitle.GetPointSize() + 2); + dc.SetFont(fontTitle); + + wxSize titleSz = dc.GetTextExtent(m_title); + dc.DrawText(m_title, padL + (w - titleSz.x) / 2, 8); + + wxFont fontNormal = wxSystemSettings::GetFont(wxSYS_DEFAULT_GUI_FONT); + dc.SetFont(fontNormal); + + // Axis titles + wxSize xLabSz = dc.GetTextExtent(m_xLabel); + dc.DrawText(m_xLabel, padL + (w - xLabSz.x) / 2, padT + h + 40); + + // Rotated Y label on left + // (90 degrees puts it reading bottom-to-top; use 270 if you prefer top-to-bottom) + dc.DrawRotatedText(m_yLabel, 15, padT + h/2 + dc.GetTextExtent(m_yLabel).x/2, 90.0); + + // Axis range labels (min/max) + for (int i = 0; i < nXTicks; ++i) { + double xv = xmin + (xmax - xmin) * static_cast(i) / static_cast(nXTicks - 1); + int px = X(xv); + + // tick mark + dc.SetPen(*wxBLACK_PEN); + dc.DrawLine(px, padT + h, px, padT + h + 5); + + // label + wxString s = FormatXValue(xv); + wxSize tsz = dc.GetTextExtent(s); + dc.DrawText(s, px - tsz.x / 2, padT + h + 12); + } + + for (int i = 0; i < nYTicks; ++i) { + double yv = ymin + (ymax - ymin) * static_cast(i) / static_cast(nYTicks - 1); + int py = Y(static_cast(yv)); + + // tick mark + dc.SetPen(*wxBLACK_PEN); + dc.DrawLine(padL - 5, py, padL, py); + + // label + wxString s = wxString::Format("%.4g", yv); + wxSize tsz = dc.GetTextExtent(s); + dc.DrawText(s, padL - 10 - tsz.x, py - tsz.y / 2); + } + + // Line plot + dc.SetPen(*wxBLACK_PEN); + wxPoint prev(X(m_x[0]), Y(m_y[0])); + for (size_t i = 1; i < m_x.size(); ++i) { + wxPoint cur(X(m_x[i]), Y(m_y[i])); + dc.DrawLine(prev, cur); + prev = cur; + } + + // Dots at each point + dc.SetPen(*wxBLACK_PEN); + dc.SetBrush(*wxBLACK_BRUSH); + const int r = 2; + for (size_t i = 0; i < m_x.size(); ++i) { + dc.DrawCircle(X(m_x[i]), Y(m_y[i]), r); + } +} + +bool wxNcVisLinePlotPanel::ExportToPNG(const wxString & filename, int width, int height) { + wxBitmap bmp(width, height); + wxMemoryDC dc(bmp); + + dc.SetBackground(wxBrush(wxSystemSettings::GetColour(wxSYS_COLOUR_BTNFACE))); + dc.Clear(); + + wxSize oldSize = GetClientSize(); + SetSize(width, height); + + DrawPlot(dc, wxSize(width, height)); + + SetSize(oldSize); + + dc.SelectObject(wxNullBitmap); + + wxImage img = bmp.ConvertToImage(); + if (!img.IsOk()) { + return false; + } + + return img.SaveFile(filename, wxBITMAP_TYPE_PNG); +} + +wxNcVisLinePlotFrame::wxNcVisLinePlotFrame( + wxWindow * parent, + const wxString & title, + const std::vector & x, + const std::vector & y, + const wxString & xLabel, + time_t baseEpoch, + const wxString & yLabel +) : wxFrame(parent, wxID_ANY, title, wxDefaultPosition, wxSize(750, 450)) +{ + wxBoxSizer * pTopSizer = new wxBoxSizer(wxVERTICAL); + wxBoxSizer * pButtonSizer = new wxBoxSizer(wxHORIZONTAL); + + wxButton * pExportBtn = new wxButton(this, ID_LinePlot_ExportPng, "Export PNG"); + pExportBtn->Bind(wxEVT_BUTTON, &wxNcVisLinePlotFrame::OnExportPng, this); + pButtonSizer->Add(pExportBtn, 0, wxALL, 5); + + m_panel = new wxNcVisLinePlotPanel(this, x, y, xLabel, baseEpoch, yLabel, title); + + pTopSizer->Add(pButtonSizer, 0, wxEXPAND); + pTopSizer->Add(m_panel, 1, wxEXPAND | wxLEFT | wxRIGHT | wxBOTTOM, 1); + + SetSizer(pTopSizer); + //SetBackgroundColour(*wxBLACK); +} + +void wxNcVisLinePlotFrame::UpdatePlot( + const wxString & title, + const std::vector & x, + const std::vector & y, + const wxString & xLabel, + time_t baseEpoch, + const wxString & yLabel +) { + SetTitle(title); + m_panel->SetData(x, y, xLabel, baseEpoch, yLabel, title); + Raise(); +} + +void wxNcVisLinePlotFrame::OnExportPng(wxCommandEvent & evt) { + wxFileDialog dlg( + this, + "Save plot as PNG", + "", + "lineplot.png", + "PNG files (*.png)|*.png", + wxFD_SAVE | wxFD_OVERWRITE_PROMPT + ); + + if (dlg.ShowModal() != wxID_OK) { + return; + } + + wxSize sz = m_panel->GetClientSize(); + if (sz.x <= 0 || sz.y <= 0) { + wxMessageBox("Invalid plot size.", "Export PNG"); + return; + } + + wxBitmap bmp(sz.x, sz.y); + wxMemoryDC memdc(bmp); + memdc.SetBackground(wxBrush(wxSystemSettings::GetColour(wxSYS_COLOUR_BTNFACE))); + memdc.Clear(); + + m_panel->Refresh(); + m_panel->Update(); + + wxClientDC srcdc(m_panel); + memdc.Blit(0, 0, sz.x, sz.y, &srcdc, 0, 0); + + memdc.SelectObject(wxNullBitmap); + + if (!m_panel->ExportToPNG(dlg.GetPath(), m_panel->GetClientSize().x, m_panel->GetClientSize().y)) { + wxMessageBox("Failed to save PNG.", "Export PNG"); + return; + } + + wxMessageBox("PNG saved successfully.", "Export PNG"); +} diff --git a/src/wxNcVisLinePlot.h b/src/wxNcVisLinePlot.h new file mode 100644 index 0000000..ff4c76c --- /dev/null +++ b/src/wxNcVisLinePlot.h @@ -0,0 +1,72 @@ +#ifndef _WXNCVISLINEPLOT_H_ +#define _WXNCVISLINEPLOT_H_ + +#include +#include +#include +#include +#include + +class wxNcVisLinePlotPanel : public wxPanel { +public: + wxNcVisLinePlotPanel( + wxWindow * parent, + const std::vector & x, + const std::vector & y, + const wxString & xLabel, + time_t baseEpoch, + const wxString & yLabel, + const wxString & title); + + void SetData( + const std::vector & x, + const std::vector & y, + const wxString & xLabel, + time_t baseEpoch, + const wxString & yLabel, + const wxString & title); + + wxString FormatXValue(double xv) const; + bool ExportToPNG(const wxString & filename, int width, int height); + +private: + void DrawPlot(wxDC & dc, const wxSize & sz); + void OnPaint(wxPaintEvent & evt); + + std::vector m_x; + std::vector m_y; + + wxString m_xLabel; + time_t m_baseEpoch; + + wxString m_yLabel; + wxString m_title; + + wxDECLARE_EVENT_TABLE(); +}; + +class wxNcVisLinePlotFrame : public wxFrame { +public: + wxNcVisLinePlotFrame( + wxWindow * parent, + const wxString & title, + const std::vector & x, + const std::vector & y, + const wxString & xLabel, + time_t baseEpoch, + const wxString & yLabel); + + void UpdatePlot( + const wxString & title, + const std::vector & x, + const std::vector & y, + const wxString & xLabel, + time_t baseEpoch, + const wxString & yLabel); + +private: + void OnExportPng(wxCommandEvent & evt); + wxNcVisLinePlotPanel * m_panel; +}; + +#endif