#include "temperature_chart.h" #include #include #include #include #include #include TemperatureChart::TemperatureChart() : drawingArea(nullptr), tooltipWindow(nullptr), tooltipLabel(nullptr), maxDataPoints(600), tickHandler(0), minTemp(MIN_TEMP), maxTemp(MAX_TEMP), minTime(0), maxTime(0), lastMouseX(-1), lastMouseY(-1), historyLengthMinutes(10) { drawingArea = gtk_drawing_area_new(); gtk_widget_set_size_request(drawingArea, 800, 400); gtk_widget_set_can_focus(drawingArea, TRUE); gtk_widget_set_focusable(drawingArea, TRUE); gtk_drawing_area_set_draw_func(GTK_DRAWING_AREA(drawingArea), [](GtkDrawingArea *area, cairo_t *cr, int width, int height, gpointer userData) { TemperatureChart *self = static_cast(userData); self->drawChart(area, cr, width, height); }, this, nullptr); // Setup event controller for mouse motion GtkEventController *motion = gtk_event_controller_motion_new(); g_signal_connect_data(motion, "motion", G_CALLBACK(+[](GtkEventControllerMotion *motion, double x, double y, gpointer userData) -> gboolean { TemperatureChart *self = static_cast(userData); self->lastMouseX = x; self->lastMouseY = y; GtkWidget *drawArea = self->drawingArea; int width = gtk_widget_get_width(drawArea); int height = gtk_widget_get_height(drawArea); NearestPoint nearest = self->findNearestDataPoint(x, y, width, height); if (nearest.found && nearest.distance < 5.0) { self->showTooltip(x, y, nearest); } else { self->hideTooltip(); } return FALSE; }), this, nullptr, (GConnectFlags)0); g_signal_connect(motion, "leave", G_CALLBACK(onLeave), this); gtk_widget_add_controller(drawingArea, motion); setupColors(); // Listen for theme changes GtkSettings *settings = gtk_settings_get_default(); if (settings) { g_signal_connect_swapped(settings, "notify::gtk-theme-name", G_CALLBACK(+[](TemperatureChart *self) { self->updateThemeColors(); gtk_widget_queue_draw(self->drawingArea); }), this); } } TemperatureChart::~TemperatureChart() { if (tickHandler) { g_source_remove(tickHandler); tickHandler = 0; } if (tooltipWindow) { gtk_widget_unparent(tooltipWindow); tooltipWindow = nullptr; tooltipLabel = nullptr; } } void TemperatureChart::setupColors() { // Define colors for different devices const GdkRGBA colors[] = { {1.0, 0.0, 0.0, 1.0}, // Red {0.0, 0.0, 1.0, 1.0}, // Blue {0.0, 0.5, 0.0, 1.0}, // Green {1.0, 0.647, 0.0, 1.0}, // Orange {0.5, 0.0, 0.5, 1.0}, // Purple {0.0, 0.5, 0.5, 1.0}, // Teal {1.0, 0.753, 0.796, 1.0}, // Pink {0.647, 0.165, 0.165, 1.0} // Brown }; int index = 0; for (auto &entry : seriesMap) { entry.second.color = colors[index % 8]; index++; } updateThemeColors(); } void TemperatureChart::updateThemeColors() { // Detect dark mode by checking the desktop theme gboolean isDark = FALSE; // Try to get the theme from both GTK settings and Gnome settings GtkSettings *settings = gtk_settings_get_default(); if (settings) { gchar *themeName = nullptr; g_object_get(settings, "gtk-theme-name", &themeName, nullptr); if (themeName) { std::string theme(themeName); // Check if theme name contains "dark" isDark = (theme.find("dark") != std::string::npos || theme.find("Dark") != std::string::npos); g_free(themeName); } } // Also try GSetting if available if (!isDark) { GSettingsSchemaSource *source = g_settings_schema_source_get_default(); if (source) { GSettingsSchema *schema = g_settings_schema_source_lookup(source, "org.gnome.desktop.interface", FALSE); if (schema) { GSettings *gsettings = g_settings_new("org.gnome.desktop.interface"); if (gsettings) { gchar *gtkTheme = g_settings_get_string(gsettings, "gtk-theme"); if (gtkTheme) { std::string theme(gtkTheme); isDark = (theme.find("dark") != std::string::npos || theme.find("Dark") != std::string::npos); g_free(gtkTheme); } g_object_unref(gsettings); } g_settings_schema_unref(schema); } } } if (isDark) { // Dark theme colors bgColor = {0.1, 0.1, 0.1, 1.0}; // Dark background textColor = {0.9, 0.9, 0.9, 1.0}; // Light text gridColor = {0.25, 0.25, 0.25, 1.0}; // Dark gray grid axisColor = {0.9, 0.9, 0.9, 1.0}; // Light axis } else { // Light theme colors bgColor = {1.0, 1.0, 1.0, 1.0}; // White background textColor = {0.0, 0.0, 0.0, 1.0}; // Black text gridColor = {0.9, 0.9, 0.9, 1.0}; // Light gray grid axisColor = {0.0, 0.0, 0.0, 1.0}; // Black axis } } GdkRGBA TemperatureChart::getColorForSeries(const std::string &seriesKey) { if (colorMap.find(seriesKey) == colorMap.end()) { const GdkRGBA colors[] = { {1.0, 0.0, 0.0, 1.0}, {0.0, 0.0, 1.0, 1.0}, {0.0, 0.5, 0.0, 1.0}, {1.0, 0.647, 0.0, 1.0}, {0.5, 0.0, 0.5, 1.0}, {0.0, 0.5, 0.5, 1.0}, {1.0, 0.753, 0.796, 1.0}, {0.647, 0.165, 0.165, 1.0} }; int colorIndex = colorMap.size() % 8; colorMap[seriesKey] = colors[colorIndex]; } return colorMap[seriesKey]; } bool TemperatureChart::addTemperatureData(const std::string &device, const std::string &sensor, double temperature, int64_t timestamp) { std::string seriesKey = device + " - " + sensor; bool isNew = false; // Create series if it doesn't exist if (seriesMap.find(seriesKey) == seriesMap.end()) { SeriesData series; series.color = getColorForSeries(seriesKey); series.id = seriesKey; series.name = seriesKey; seriesMap[seriesKey] = series; isNew = true; } // Add data point DataPoint point; point.temperature = temperature; point.timestamp = timestamp; seriesMap[seriesKey].points.push_back(point); // Keep only historyLengthMinutes of data int64_t cutoffTime = timestamp - (static_cast(historyLengthMinutes) * 60 * 1000); for (auto &entry : seriesMap) { auto &points = entry.second.points; auto it = points.begin(); while (it != points.end() && it->timestamp < cutoffTime) { it = points.erase(it); } } // Update temperature range dynamically double currentMin = 1000.0; double currentMax = -1000.0; bool hasData = 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; hasData = true; } } if (hasData) { // Round down min to nearest 10, round up max to nearest 10 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; } // Don't let it be too small if (minTemp > 30.0) minTemp = 30.0; } else { minTemp = MIN_TEMP; maxTemp = MAX_TEMP; } // Update time range - keep dynamic window maxTime = timestamp; minTime = timestamp - (static_cast(historyLengthMinutes) * 60 * 1000); // Trigger redraw gtk_widget_queue_draw(drawingArea); return isNew; } void TemperatureChart::clear() { seriesMap.clear(); colorMap.clear(); minTemp = MIN_TEMP; maxTemp = MAX_TEMP; minTime = 0; maxTime = 0; gtk_widget_queue_draw(drawingArea); } std::vector> TemperatureChart::getSeriesColors() const { std::vector> result; for (const auto &pair : seriesMap) { result.push_back({pair.first, pair.second.color}); } return result; } void TemperatureChart::setSeriesColor(const std::string &seriesName, const GdkRGBA &color) { // Update color map colorMap[seriesName] = color; // Update series if it exists if (seriesMap.find(seriesName) != seriesMap.end()) { seriesMap[seriesName].color = color; } 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(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 { auto it = seriesMap.find(seriesId); if (it != seriesMap.end()) { return it->second.name; } 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) { if (seriesMap.find(seriesId) != seriesMap.end()) { seriesMap[seriesId].name = name; } } void TemperatureChart::drawChart(GtkDrawingArea *area, cairo_t *cr, int width, int height) { // Background with theme color cairo_set_source_rgb(cr, bgColor.red, bgColor.green, bgColor.blue); cairo_paint(cr); if (seriesMap.empty() || maxTime == minTime) { // Draw placeholder cairo_set_source_rgb(cr, textColor.red * 0.6, textColor.green * 0.6, textColor.blue * 0.6); cairo_select_font_face(cr, "Sans", CAIRO_FONT_SLANT_NORMAL, CAIRO_FONT_WEIGHT_NORMAL); cairo_set_font_size(cr, 14); cairo_move_to(cr, width / 2 - 100, height / 2); cairo_show_text(cr, "Waiting for data..."); return; } // Margins double marginLeft = 20, marginRight = 70, marginTop = 40, marginBottom = 40; double plotWidth = width - marginLeft - marginRight; double plotHeight = height - marginTop - marginBottom; // Draw grid and axes cairo_set_source_rgb(cr, gridColor.red, gridColor.green, gridColor.blue); cairo_set_line_width(cr, 0.5); // Y axis grid lines (temperature) - every 10 degrees for (double temp = minTemp; temp <= maxTemp; temp += 10.0) { double y = marginTop + plotHeight * (1.0 - (temp - minTemp) / (maxTemp - minTemp)); cairo_move_to(cr, marginLeft, y); cairo_line_to(cr, marginLeft + plotWidth, y); cairo_stroke(cr); } // X axis grid lines (time) - 5 lines for 10-minute window for (int i = 0; i <= 5; i++) { double x = marginLeft + (plotWidth * i / 5.0); cairo_move_to(cr, x, marginTop); cairo_line_to(cr, x, marginTop + plotHeight); cairo_stroke(cr); } // Draw axes cairo_set_source_rgb(cr, axisColor.red, axisColor.green, axisColor.blue); cairo_set_line_width(cr, 2); cairo_move_to(cr, marginLeft + plotWidth, marginTop); cairo_line_to(cr, marginLeft + plotWidth, marginTop + plotHeight); cairo_line_to(cr, marginLeft, marginTop + plotHeight); cairo_stroke(cr); // Draw axis labels cairo_set_source_rgb(cr, textColor.red, textColor.green, textColor.blue); cairo_select_font_face(cr, "Sans", CAIRO_FONT_SLANT_NORMAL, CAIRO_FONT_WEIGHT_NORMAL); cairo_set_font_size(cr, 10); // Temperature labels every 10 degrees on Y axis for (double temp = minTemp; temp <= maxTemp; temp += 10.0) { double y = marginTop + plotHeight * (1.0 - (temp - minTemp) / (maxTemp - minTemp)); char tempStr[16]; snprintf(tempStr, sizeof(tempStr), "%.0f°C", temp); // Measure text width for proper alignment cairo_text_extents_t extents; cairo_text_extents(cr, tempStr, &extents); // Position text 8 pixels after the plot area on the right double textX = marginLeft + plotWidth + 8; cairo_move_to(cr, textX, y + 4); cairo_show_text(cr, tempStr); } // Time labels on X axis (5 points for history window) for (int i = 0; i <= 5; i++) { double x = marginLeft + (plotWidth * i / 5.0); // Calculate time offset in minutes int minutes = (5 - i) * historyLengthMinutes / 5; char timeStr[16]; snprintf(timeStr, sizeof(timeStr), "-%d m", minutes); cairo_move_to(cr, x - 20, marginTop + plotHeight + 20); cairo_show_text(cr, timeStr); } // Draw data series (right to left, with history window) for (auto &seriesEntry : seriesMap) { SeriesData &series = seriesEntry.second; if (!series.visible || series.points.empty()) continue; // Set series color cairo_set_source_rgba(cr, series.color.red, series.color.green, series.color.blue, series.color.alpha); cairo_set_line_width(cr, 2.0); bool firstPoint = true; for (const DataPoint &point : series.points) { // Reverse X axis - newer data on the right, older on the left int64_t historyMs = static_cast(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)); if (firstPoint) { cairo_move_to(cr, x, y); firstPoint = false; } else { cairo_line_to(cr, x, y); } } cairo_stroke(cr); } // Draw title cairo_set_source_rgb(cr, textColor.red, textColor.green, textColor.blue); cairo_select_font_face(cr, "Sans", CAIRO_FONT_SLANT_NORMAL, CAIRO_FONT_WEIGHT_BOLD); cairo_set_font_size(cr, 16); cairo_move_to(cr, width / 2 - 150, 25); cairo_show_text(cr, "NVMe Disk Temperatures (Real-time)"); } TemperatureChart::NearestPoint TemperatureChart::findNearestDataPoint(double mouseX, double mouseY, int width, int height) { NearestPoint result; result.found = false; result.distance = std::numeric_limits::max(); if (seriesMap.empty() || maxTime == minTime) { return result; } // Same margins as in drawChart double marginLeft = 20, marginRight = 70, marginTop = 40, marginBottom = 40; double plotWidth = width - marginLeft - marginRight; double plotHeight = height - marginTop - marginBottom; // Check if mouse is in plot area if (mouseX < marginLeft || mouseX > marginLeft + plotWidth || mouseY < marginTop || mouseY > marginTop + plotHeight) { return result; } for (auto &seriesEntry : seriesMap) { SeriesData &series = seriesEntry.second; if (!series.visible) continue; for (const DataPoint &point : series.points) { int64_t historyMs = static_cast(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 distance = std::hypot(mouseX - x, mouseY - y); if (distance < result.distance) { result.distance = distance; result.found = true; result.temperature = point.temperature; result.timestamp = point.timestamp; result.seriesName = series.name; } } } return result; } void TemperatureChart::showTooltip(double x, double y, const NearestPoint &point) { if (!tooltipWindow) { tooltipWindow = gtk_popover_new(); tooltipLabel = gtk_label_new(""); gtk_widget_set_margin_start(tooltipLabel, 8); gtk_widget_set_margin_end(tooltipLabel, 8); gtk_widget_set_margin_top(tooltipLabel, 5); gtk_widget_set_margin_bottom(tooltipLabel, 5); gtk_popover_set_child(GTK_POPOVER(tooltipWindow), tooltipLabel); gtk_popover_set_position(GTK_POPOVER(tooltipWindow), GTK_POS_TOP); gtk_popover_set_has_arrow(GTK_POPOVER(tooltipWindow), TRUE); gtk_popover_set_autohide(GTK_POPOVER(tooltipWindow), FALSE); gtk_widget_set_parent(tooltipWindow, drawingArea); } // Format the tooltip text with real current time char tooltipText[256]; // Convert timestamp from milliseconds since epoch to time structure time_t timeSeconds = point.timestamp / 1000; struct tm *timeinfo = localtime(&timeSeconds); char timeStr[32] = "??:??:??"; if (timeinfo) { strftime(timeStr, sizeof(timeStr), "%H:%M:%S", timeinfo); } snprintf(tooltipText, sizeof(tooltipText), "%s\n%.1f°C\n%s", point.seriesName.c_str(), point.temperature, timeStr); gtk_label_set_text(GTK_LABEL(tooltipLabel), tooltipText); // Position the tooltip GdkRectangle rect; rect.x = (int)x; rect.y = (int)y; rect.width = 1; rect.height = 1; gtk_popover_set_pointing_to(GTK_POPOVER(tooltipWindow), &rect); if (!gtk_widget_get_visible(tooltipWindow)) { gtk_widget_set_visible(tooltipWindow, TRUE); } } void TemperatureChart::hideTooltip() { if (tooltipWindow && gtk_widget_get_visible(tooltipWindow)) { gtk_widget_set_visible(tooltipWindow, FALSE); } } void TemperatureChart::onLeave(GtkEventControllerMotion *motion, gpointer userData) { TemperatureChart *self = static_cast(userData); self->hideTooltip(); }