refactor, new features

This commit is contained in:
rdavidek 2026-01-14 19:48:00 +01:00
parent 376d0a17e8
commit 14e22cc636
8 changed files with 210 additions and 27 deletions

View File

@ -1,5 +1,5 @@
cmake_minimum_required(VERSION 3.16) cmake_minimum_required(VERSION 3.16)
project(nvme-monitor) project(temp-monitor)
set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fPIC") set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fPIC")
@ -44,19 +44,19 @@ set(HEADERS
include/config_manager.h include/config_manager.h
) )
add_executable(nvme-monitor ${SOURCES} ${HEADERS}) add_executable(temp-monitor ${SOURCES} ${HEADERS})
target_link_libraries(nvme-monitor target_link_libraries(temp-monitor
${GTK_LIBRARIES} ${GTK_LIBRARIES}
${CAIRO_LIBRARIES} ${CAIRO_LIBRARIES}
m m
) )
target_compile_options(nvme-monitor PRIVATE ${GTK_CFLAGS_OTHER}) target_compile_options(temp-monitor PRIVATE ${GTK_CFLAGS_OTHER})
target_compile_options(nvme-monitor PRIVATE ${CAIRO_CFLAGS_OTHER}) target_compile_options(temp-monitor PRIVATE ${CAIRO_CFLAGS_OTHER})
# Install executable # Install executable
install(TARGETS nvme-monitor install(TARGETS temp-monitor
DESTINATION bin) DESTINATION bin)
# Install desktop file for system integration # Install desktop file for system integration

View File

@ -8,4 +8,4 @@ cd build
cmake .. cmake ..
make make
echo "Build complete! Run with: ./nvme-monitor" echo "Build complete! Run with: ./temp-monitor"

View File

@ -18,16 +18,20 @@ public:
int getWindowWidth() const { return windowWidth; } int getWindowWidth() const { return windowWidth; }
int getWindowHeight() const { return windowHeight; } int getWindowHeight() const { return windowHeight; }
int getPollingTime() const { return pollingTime; } int getPollingTime() const { return pollingTime; }
int getHistoryLength() const { return historyLength; }
void setWindowWidth(int w) { windowWidth = w; } void setWindowWidth(int w) { windowWidth = w; }
void setWindowHeight(int h) { windowHeight = h; } void setWindowHeight(int h) { windowHeight = h; }
void setPollingTime(int t) { pollingTime = t; } void setPollingTime(int t) { pollingTime = t; }
void setHistoryLength(int m) { historyLength = m; }
std::map<std::string, std::string> getSensorColors() const { return sensorColors; } std::map<std::string, std::string> getSensorColors() const { return sensorColors; }
std::map<std::string, std::string> getSensorNames() const { return sensorNames; } std::map<std::string, std::string> getSensorNames() const { return sensorNames; }
std::map<std::string, bool> getSensorEnabled() const { return sensorEnabled; }
void setSensorColor(const std::string &id, const std::string &color) { sensorColors[id] = color; } void setSensorColor(const std::string &id, const std::string &color) { sensorColors[id] = color; }
void setSensorName(const std::string &id, const std::string &name) { sensorNames[id] = name; } void setSensorName(const std::string &id, const std::string &name) { sensorNames[id] = name; }
void setSensorEnabled(const std::string &id, bool enabled) { sensorEnabled[id] = enabled; }
private: private:
std::string getConfigFilePath() const; std::string getConfigFilePath() const;
@ -36,9 +40,11 @@ private:
int windowWidth; int windowWidth;
int windowHeight; int windowHeight;
int pollingTime; int pollingTime;
int historyLength;
std::map<std::string, std::string> sensorColors; std::map<std::string, std::string> sensorColors;
std::map<std::string, std::string> sensorNames; std::map<std::string, std::string> sensorNames;
std::map<std::string, bool> sensorEnabled;
}; };
#endif // CONFIG_MANAGER_H #endif // CONFIG_MANAGER_H

View File

@ -24,14 +24,17 @@ private:
static gboolean onDeleteWindow(GtkWidget *widget, gpointer userData); static gboolean onDeleteWindow(GtkWidget *widget, gpointer userData);
static gboolean onUpdateTimer(gpointer userData); static gboolean onUpdateTimer(gpointer userData);
static void onRefreshRateChanged(GtkSpinButton *spinButton, gpointer userData); static void onRefreshRateChanged(GtkSpinButton *spinButton, gpointer userData);
static void onHistoryLengthChanged(GtkSpinButton *spinButton, gpointer userData);
static void onClearButtonClicked(GtkButton *button, gpointer userData); static void onClearButtonClicked(GtkButton *button, gpointer userData);
static void onQuitButtonClicked(GtkButton *button, gpointer userData); static void onQuitButtonClicked(GtkButton *button, gpointer userData);
static void onColorSet(GObject *object, GParamSpec *pspec, gpointer userData); static void onColorSet(GObject *object, GParamSpec *pspec, gpointer userData);
static void onNameChanged(GtkEditable *editable, gpointer userData); static void onNameChanged(GtkEditable *editable, gpointer userData);
static void onVisibilityToggled(GtkCheckButton *checkButton, gpointer userData);
GtkWidget *window; GtkWidget *window;
GtkWidget *statusLabel; GtkWidget *statusLabel;
GtkSpinButton *refreshRateSpinBox; GtkSpinButton *refreshRateSpinBox;
GtkSpinButton *historyLengthSpinBox;
std::unique_ptr<TemperatureChart> chart; std::unique_ptr<TemperatureChart> chart;
std::unique_ptr<TempMonitor> monitor; std::unique_ptr<TempMonitor> monitor;
std::unique_ptr<ConfigManager> config; std::unique_ptr<ConfigManager> config;

View File

@ -18,6 +18,7 @@ struct SeriesData {
GdkRGBA color; GdkRGBA color;
std::string id; // Unique internal ID (device + sensor) std::string id; // Unique internal ID (device + sensor)
std::string name; // User-friendly display name std::string name; // User-friendly display name
bool visible = true;
}; };
class TemperatureChart { class TemperatureChart {
@ -40,6 +41,12 @@ public:
std::string getSeriesName(const std::string &seriesId) const; std::string getSeriesName(const std::string &seriesId) const;
void setSeriesName(const std::string &seriesId, const std::string &name); void setSeriesName(const std::string &seriesId, const std::string &name);
// Visibility management
bool isSeriesVisible(const std::string &seriesId) const;
void setSeriesVisible(const std::string &seriesId, bool visible);
void setHistoryLength(int minutes);
void drawChart(GtkDrawingArea *area, cairo_t *cr, int width, int height); void drawChart(GtkDrawingArea *area, cairo_t *cr, int width, int height);
private: private:
@ -75,7 +82,7 @@ private:
// Cairo drawing state // Cairo drawing state
double minTemp, maxTemp; double minTemp, maxTemp;
int64_t minTime, maxTime; int64_t minTime, maxTime;
static constexpr int MAX_TIME_MS = 600000; // 10 minutes in milliseconds int historyLengthMinutes;
static constexpr double MIN_TEMP = 10.0; static constexpr double MIN_TEMP = 10.0;
static constexpr double MAX_TEMP = 110.0; static constexpr double MAX_TEMP = 110.0;

View File

@ -7,7 +7,7 @@
#include <unistd.h> #include <unistd.h>
ConfigManager::ConfigManager() ConfigManager::ConfigManager()
: windowWidth(1200), windowHeight(700), pollingTime(1) : windowWidth(1200), windowHeight(700), pollingTime(1), historyLength(10)
{ {
} }
@ -23,7 +23,7 @@ std::string ConfigManager::getConfigFilePath() const
if (len == -1) { if (len == -1) {
// Fallback to current directory // Fallback to current directory
cachedConfigPath = "./nvme-monitor.conf"; cachedConfigPath = "./temp-monitor.conf";
return cachedConfigPath; return cachedConfigPath;
} }
@ -34,11 +34,11 @@ std::string ConfigManager::getConfigFilePath() const
size_t lastSlash = fullPath.find_last_of('/'); size_t lastSlash = fullPath.find_last_of('/');
if (lastSlash != std::string::npos) { if (lastSlash != std::string::npos) {
std::string exeDir = fullPath.substr(0, lastSlash); std::string exeDir = fullPath.substr(0, lastSlash);
cachedConfigPath = exeDir + "/nvme-monitor.conf"; cachedConfigPath = exeDir + "/temp-monitor.conf";
return cachedConfigPath; return cachedConfigPath;
} }
cachedConfigPath = "./nvme-monitor.conf"; cachedConfigPath = "./temp-monitor.conf";
return cachedConfigPath; return cachedConfigPath;
} }
@ -75,10 +75,14 @@ void ConfigManager::load()
windowHeight = std::stoi(value); windowHeight = std::stoi(value);
} else if (key == "polling_time") { } else if (key == "polling_time") {
pollingTime = std::stoi(value); pollingTime = std::stoi(value);
} else if (key == "history_length") {
historyLength = std::stoi(value);
} else if (key.find("color_") == 0) { } else if (key.find("color_") == 0) {
sensorColors[key.substr(6)] = value; sensorColors[key.substr(6)] = value;
} else if (key.find("name_") == 0) { } else if (key.find("name_") == 0) {
sensorNames[key.substr(5)] = value; sensorNames[key.substr(5)] = value;
} else if (key.find("enabled_") == 0) {
sensorEnabled[key.substr(8)] = (value == "true" || value == "1");
} }
} catch (const std::exception &e) { } catch (const std::exception &e) {
std::cerr << "Config error: invalid value for '" << key << "': " << value << " (" << e.what() << ")" << std::endl; std::cerr << "Config error: invalid value for '" << key << "': " << value << " (" << e.what() << ")" << std::endl;
@ -104,6 +108,7 @@ void ConfigManager::save()
file << "window_width = " << windowWidth << "\n"; file << "window_width = " << windowWidth << "\n";
file << "window_height = " << windowHeight << "\n"; file << "window_height = " << windowHeight << "\n";
file << "polling_time = " << pollingTime << "\n"; file << "polling_time = " << pollingTime << "\n";
file << "history_length = " << historyLength << "\n";
for (auto const& [id, color] : sensorColors) { for (auto const& [id, color] : sensorColors) {
file << "color_" << id << " = " << color << "\n"; file << "color_" << id << " = " << color << "\n";
@ -113,5 +118,9 @@ void ConfigManager::save()
file << "name_" << id << " = " << name << "\n"; file << "name_" << id << " = " << name << "\n";
} }
for (auto const& [id, enabled] : sensorEnabled) {
file << "enabled_" << id << " = " << (enabled ? "true" : "false") << "\n";
}
file.close(); file.close();
} }

View File

@ -8,7 +8,8 @@
#include <gdk-pixbuf/gdk-pixbuf.h> #include <gdk-pixbuf/gdk-pixbuf.h>
MainWindow::MainWindow() MainWindow::MainWindow()
: window(nullptr), statusLabel(nullptr), refreshRateSpinBox(nullptr), : window(nullptr), statusLabel(nullptr),
refreshRateSpinBox(nullptr), historyLengthSpinBox(nullptr),
timerID(0), refreshRateSec(3) timerID(0), refreshRateSec(3)
{ {
monitor = std::make_unique<TempMonitor>(); monitor = std::make_unique<TempMonitor>();
@ -19,6 +20,12 @@ MainWindow::MainWindow()
setupUI(); setupUI();
int historyLen = config->getHistoryLength();
chart->setHistoryLength(historyLen);
if (historyLengthSpinBox) {
gtk_spin_button_set_value(historyLengthSpinBox, historyLen);
}
// Set window size from config // Set window size from config
gtk_window_set_default_size(GTK_WINDOW(window), config->getWindowWidth(), config->getWindowHeight()); gtk_window_set_default_size(GTK_WINDOW(window), config->getWindowWidth(), config->getWindowHeight());
@ -50,7 +57,7 @@ void MainWindow::setupUI()
g_object_unref(provider); g_object_unref(provider);
window = gtk_window_new(); window = gtk_window_new();
gtk_window_set_title(GTK_WINDOW(window), "NVMe Temperature Monitor"); gtk_window_set_title(GTK_WINDOW(window), "Temperature Monitor");
// Set window icon - in GTK4 we load pixbuf and use it for the window display // Set window icon - in GTK4 we load pixbuf and use it for the window display
GError *error = nullptr; GError *error = nullptr;
@ -92,6 +99,17 @@ void MainWindow::setupUI()
g_signal_connect(refreshRateSpinBox, "value-changed", G_CALLBACK(onRefreshRateChanged), this); g_signal_connect(refreshRateSpinBox, "value-changed", G_CALLBACK(onRefreshRateChanged), this);
gtk_box_append(GTK_BOX(controlBox), GTK_WIDGET(refreshRateSpinBox)); gtk_box_append(GTK_BOX(controlBox), GTK_WIDGET(refreshRateSpinBox));
// History length label
GtkWidget *historyLabel = gtk_label_new("History (min):");
gtk_box_append(GTK_BOX(controlBox), historyLabel);
// History length spinner (5-60 min, step 5)
GtkAdjustment *histAdjustment = gtk_adjustment_new(10, 5, 60, 5, 10, 0);
historyLengthSpinBox = GTK_SPIN_BUTTON(gtk_spin_button_new(histAdjustment, 5, 0));
gtk_widget_set_size_request(GTK_WIDGET(historyLengthSpinBox), 80, -1);
g_signal_connect(historyLengthSpinBox, "value-changed", G_CALLBACK(onHistoryLengthChanged), this);
gtk_box_append(GTK_BOX(controlBox), GTK_WIDGET(historyLengthSpinBox));
// Clear button // Clear button
GtkWidget *clearButton = gtk_button_new_with_label("Clear Data"); GtkWidget *clearButton = gtk_button_new_with_label("Clear Data");
g_signal_connect(clearButton, "clicked", G_CALLBACK(onClearButtonClicked), this); g_signal_connect(clearButton, "clicked", G_CALLBACK(onClearButtonClicked), this);
@ -161,6 +179,14 @@ void MainWindow::onRefreshRateChanged(GtkSpinButton *spinButton, gpointer userDa
self->timerID = g_timeout_add(self->refreshRateSec * 1000, onUpdateTimer, self); self->timerID = g_timeout_add(self->refreshRateSec * 1000, onUpdateTimer, self);
} }
void MainWindow::onHistoryLengthChanged(GtkSpinButton *spinButton, gpointer userData)
{
MainWindow *self = static_cast<MainWindow*>(userData);
int minutes = gtk_spin_button_get_value_as_int(spinButton);
self->chart->setHistoryLength(minutes);
self->config->setHistoryLength(minutes);
}
void MainWindow::onClearButtonClicked(GtkButton *button, gpointer userData) void MainWindow::onClearButtonClicked(GtkButton *button, gpointer userData)
{ {
MainWindow *self = static_cast<MainWindow*>(userData); MainWindow *self = static_cast<MainWindow*>(userData);
@ -273,15 +299,30 @@ void MainWindow::updateLegend()
auto seriesColors = chart->getSeriesColors(); auto seriesColors = chart->getSeriesColors();
// Add legend items // Add legend items
auto sensorEnabledMap = config->getSensorEnabled();
for (const auto &pair : seriesColors) { for (const auto &pair : seriesColors) {
const std::string &seriesId = pair.first; const std::string &seriesId = pair.first;
const GdkRGBA &color = pair.second; const GdkRGBA &color = pair.second;
std::string displayName = chart->getSeriesName(seriesId); std::string displayName = chart->getSeriesName(seriesId);
bool isVisible = true;
if (sensorEnabledMap.find(seriesId) != sensorEnabledMap.end()) {
isVisible = sensorEnabledMap[seriesId];
}
chart->setSeriesVisible(seriesId, isVisible);
// Create container for legend item // Create container for legend item
GtkWidget *itemBox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 2); GtkWidget *itemBox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 2);
gtk_widget_add_css_class(itemBox, "legend-item"); gtk_widget_add_css_class(itemBox, "legend-item");
// Checkbox for visibility
GtkWidget *checkButton = gtk_check_button_new();
gtk_check_button_set_active(GTK_CHECK_BUTTON(checkButton), isVisible);
g_object_set_data_full(G_OBJECT(checkButton), "series-name", g_strdup(seriesId.c_str()), g_free);
g_signal_connect(checkButton, "toggled", G_CALLBACK(onVisibilityToggled), this);
gtk_box_append(GTK_BOX(itemBox), checkButton);
// Create color dialog and button // Create color dialog and button
GtkColorDialog *dialog = gtk_color_dialog_new(); GtkColorDialog *dialog = gtk_color_dialog_new();
GtkWidget *colorButton = gtk_color_dialog_button_new(dialog); GtkWidget *colorButton = gtk_color_dialog_button_new(dialog);
@ -354,3 +395,16 @@ void MainWindow::onNameChanged(GtkEditable *editable, gpointer userData)
} }
} }
} }
void MainWindow::onVisibilityToggled(GtkCheckButton *checkButton, gpointer userData)
{
MainWindow *self = static_cast<MainWindow*>(userData);
const char *seriesId = static_cast<const char*>(g_object_get_data(G_OBJECT(checkButton), "series-name"));
if (seriesId) {
bool visible = gtk_check_button_get_active(checkButton);
self->chart->setSeriesVisible(seriesId, visible);
self->config->setSensorEnabled(seriesId, visible);
self->config->save();
}
}

View File

@ -10,7 +10,7 @@ TemperatureChart::TemperatureChart()
: drawingArea(nullptr), tooltipWindow(nullptr), tooltipLabel(nullptr), : drawingArea(nullptr), tooltipWindow(nullptr), tooltipLabel(nullptr),
maxDataPoints(600), tickHandler(0), maxDataPoints(600), tickHandler(0),
minTemp(MIN_TEMP), maxTemp(MAX_TEMP), minTime(0), maxTime(0), minTemp(MIN_TEMP), maxTemp(MAX_TEMP), minTime(0), maxTime(0),
lastMouseX(-1), lastMouseY(-1) lastMouseX(-1), lastMouseY(-1), historyLengthMinutes(10)
{ {
drawingArea = gtk_drawing_area_new(); drawingArea = gtk_drawing_area_new();
gtk_widget_set_size_request(drawingArea, 800, 400); gtk_widget_set_size_request(drawingArea, 800, 400);
@ -200,8 +200,8 @@ bool TemperatureChart::addTemperatureData(const std::string &device, const std::
seriesMap[seriesKey].points.push_back(point); seriesMap[seriesKey].points.push_back(point);
// Keep only last 10 minutes of data // Keep only historyLengthMinutes of data
int64_t cutoffTime = timestamp - MAX_TIME_MS; int64_t cutoffTime = timestamp - (static_cast<int64_t>(historyLengthMinutes) * 60 * 1000);
for (auto &entry : seriesMap) { for (auto &entry : seriesMap) {
auto &points = entry.second.points; auto &points = entry.second.points;
auto it = points.begin(); auto it = points.begin();
@ -216,6 +216,7 @@ bool TemperatureChart::addTemperatureData(const std::string &device, const std::
bool hasData = false; bool hasData = false;
for (const auto &entry : seriesMap) { for (const auto &entry : seriesMap) {
if (!entry.second.visible) continue;
for (const auto &p : entry.second.points) { for (const auto &p : entry.second.points) {
if (p.temperature < currentMin) currentMin = p.temperature; if (p.temperature < currentMin) currentMin = p.temperature;
if (p.temperature > currentMax) currentMax = p.temperature; if (p.temperature > currentMax) currentMax = p.temperature;
@ -240,9 +241,9 @@ bool TemperatureChart::addTemperatureData(const std::string &device, const std::
maxTemp = MAX_TEMP; maxTemp = MAX_TEMP;
} }
// Update time range - keep 10 minute window // Update time range - keep dynamic window
maxTime = timestamp; maxTime = timestamp;
minTime = timestamp - MAX_TIME_MS; minTime = timestamp - (static_cast<int64_t>(historyLengthMinutes) * 60 * 1000);
// Trigger redraw // Trigger redraw
gtk_widget_queue_draw(drawingArea); gtk_widget_queue_draw(drawingArea);
@ -283,6 +284,67 @@ void TemperatureChart::setSeriesColor(const std::string &seriesName, const GdkRG
gtk_widget_queue_draw(drawingArea); gtk_widget_queue_draw(drawingArea);
} }
void TemperatureChart::setHistoryLength(int minutes)
{
historyLengthMinutes = minutes;
// Immediately recalculate ranges based on new history length
if (!seriesMap.empty()) {
int64_t latestTimestamp = 0;
for (const auto &entry : seriesMap) {
if (!entry.second.points.empty()) {
latestTimestamp = std::max(latestTimestamp, entry.second.points.back().timestamp);
}
}
if (latestTimestamp > 0) {
int64_t cutoffTime = latestTimestamp - (static_cast<int64_t>(historyLengthMinutes) * 60 * 1000);
double currentMin = 1000.0;
double currentMax = -1000.0;
bool hasData = false;
for (auto &entry : seriesMap) {
auto &points = entry.second.points;
if (!entry.second.visible) continue;
// Prune points that are now outside the new history window
auto it = points.begin();
while (it != points.end() && it->timestamp < cutoffTime) {
it = points.erase(it);
}
for (const auto &p : points) {
if (p.temperature < currentMin) currentMin = p.temperature;
if (p.temperature > currentMax) currentMax = p.temperature;
hasData = true;
}
}
if (hasData) {
minTemp = std::floor(currentMin / 10.0) * 10.0;
maxTemp = std::ceil(currentMax / 10.0) * 10.0;
// Ensure at least 20 degrees range
if (maxTemp - minTemp < 20.0) {
maxTemp = minTemp + 20.0;
}
if (minTemp > 30.0) minTemp = 30.0;
} else {
minTemp = MIN_TEMP;
maxTemp = MAX_TEMP;
}
maxTime = latestTimestamp;
minTime = cutoffTime;
}
}
gtk_widget_queue_draw(drawingArea);
}
std::string TemperatureChart::getSeriesName(const std::string &seriesId) const std::string TemperatureChart::getSeriesName(const std::string &seriesId) const
{ {
auto it = seriesMap.find(seriesId); auto it = seriesMap.find(seriesId);
@ -292,6 +354,45 @@ std::string TemperatureChart::getSeriesName(const std::string &seriesId) const
return seriesId; return seriesId;
} }
bool TemperatureChart::isSeriesVisible(const std::string &seriesId) const
{
auto it = seriesMap.find(seriesId);
if (it != seriesMap.end()) {
return it->second.visible;
}
return true;
}
void TemperatureChart::setSeriesVisible(const std::string &seriesId, bool visible)
{
if (seriesMap.find(seriesId) != seriesMap.end()) {
seriesMap[seriesId].visible = visible;
// Recalculate temperature range
double currentMin = 1000.0;
double currentMax = -1000.0;
bool hasVisibleData = false;
for (const auto &entry : seriesMap) {
if (!entry.second.visible) continue;
for (const auto &p : entry.second.points) {
if (p.temperature < currentMin) currentMin = p.temperature;
if (p.temperature > currentMax) currentMax = p.temperature;
hasVisibleData = true;
}
}
if (hasVisibleData) {
minTemp = std::floor(currentMin / 10.0) * 10.0;
maxTemp = std::ceil(currentMax / 10.0) * 10.0;
if (maxTemp - minTemp < 20.0) maxTemp = minTemp + 20.0;
if (minTemp > 30.0) minTemp = 30.0;
}
gtk_widget_queue_draw(drawingArea);
}
}
void TemperatureChart::setSeriesName(const std::string &seriesId, const std::string &name) void TemperatureChart::setSeriesName(const std::string &seriesId, const std::string &name)
{ {
if (seriesMap.find(seriesId) != seriesMap.end()) { if (seriesMap.find(seriesId) != seriesMap.end()) {
@ -371,13 +472,12 @@ void TemperatureChart::drawChart(GtkDrawingArea *area, cairo_t *cr, int width, i
cairo_show_text(cr, tempStr); cairo_show_text(cr, tempStr);
} }
// Time labels on X axis (5 points for 10-minute window) // Time labels on X axis (5 points for history window)
for (int i = 0; i <= 5; i++) { for (int i = 0; i <= 5; i++) {
double x = marginLeft + (plotWidth * i / 5.0); double x = marginLeft + (plotWidth * i / 5.0);
// Calculate time offset in seconds // Calculate time offset in minutes
int secondsOffset = (5 - i) * 120; // 10 minutes / 5 = 2 minutes between points int minutes = (5 - i) * historyLengthMinutes / 5;
int minutes = secondsOffset / 60;
char timeStr[16]; char timeStr[16];
snprintf(timeStr, sizeof(timeStr), "-%d m", minutes); snprintf(timeStr, sizeof(timeStr), "-%d m", minutes);
@ -386,11 +486,11 @@ void TemperatureChart::drawChart(GtkDrawingArea *area, cairo_t *cr, int width, i
cairo_show_text(cr, timeStr); cairo_show_text(cr, timeStr);
} }
// Draw data series (right to left, with 10-minute window) // Draw data series (right to left, with history window)
for (auto &seriesEntry : seriesMap) { for (auto &seriesEntry : seriesMap) {
SeriesData &series = seriesEntry.second; SeriesData &series = seriesEntry.second;
if (series.points.empty()) continue; if (!series.visible || series.points.empty()) continue;
// Set series color // Set series color
cairo_set_source_rgba(cr, series.color.red, series.color.green, cairo_set_source_rgba(cr, series.color.red, series.color.green,
@ -400,7 +500,8 @@ void TemperatureChart::drawChart(GtkDrawingArea *area, cairo_t *cr, int width, i
bool firstPoint = true; bool firstPoint = true;
for (const DataPoint &point : series.points) { for (const DataPoint &point : series.points) {
// Reverse X axis - newer data on the right, older on the left // Reverse X axis - newer data on the right, older on the left
double x = marginLeft + plotWidth * (1.0 - (double)(maxTime - point.timestamp) / MAX_TIME_MS); int64_t historyMs = static_cast<int64_t>(historyLengthMinutes) * 60 * 1000;
double x = marginLeft + plotWidth * (1.0 - (double)(maxTime - point.timestamp) / historyMs);
double y = marginTop + plotHeight * (1.0 - (point.temperature - minTemp) / (maxTemp - minTemp)); double y = marginTop + plotHeight * (1.0 - (point.temperature - minTemp) / (maxTemp - minTemp));
if (firstPoint) { if (firstPoint) {
@ -444,8 +545,11 @@ TemperatureChart::NearestPoint TemperatureChart::findNearestDataPoint(double mou
for (auto &seriesEntry : seriesMap) { for (auto &seriesEntry : seriesMap) {
SeriesData &series = seriesEntry.second; SeriesData &series = seriesEntry.second;
if (!series.visible) continue;
for (const DataPoint &point : series.points) { for (const DataPoint &point : series.points) {
double x = marginLeft + plotWidth * (1.0 - (double)(maxTime - point.timestamp) / MAX_TIME_MS); int64_t historyMs = static_cast<int64_t>(historyLengthMinutes) * 60 * 1000;
double x = marginLeft + plotWidth * (1.0 - (double)(maxTime - point.timestamp) / historyMs);
double y = marginTop + plotHeight * (1.0 - (point.temperature - minTemp) / (maxTemp - minTemp)); double y = marginTop + plotHeight * (1.0 - (point.temperature - minTemp) / (maxTemp - minTemp));
double distance = std::hypot(mouseX - x, mouseY - y); double distance = std::hypot(mouseX - x, mouseY - y);