527 lines
18 KiB
C++
527 lines
18 KiB
C++
#include "temperature_chart.h"
|
|
#include <iostream>
|
|
#include <cmath>
|
|
#include <ctime>
|
|
#include <algorithm>
|
|
#include <limits>
|
|
#include <string>
|
|
|
|
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)
|
|
{
|
|
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<TemperatureChart*>(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<TemperatureChart*>(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 last 10 minutes of data
|
|
int64_t cutoffTime = timestamp - MAX_TIME_MS;
|
|
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) {
|
|
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 10 minute window
|
|
maxTime = timestamp;
|
|
minTime = timestamp - MAX_TIME_MS;
|
|
|
|
// 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<std::pair<std::string, GdkRGBA>> TemperatureChart::getSeriesColors() const
|
|
{
|
|
std::vector<std::pair<std::string, GdkRGBA>> 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);
|
|
}
|
|
|
|
std::string TemperatureChart::getSeriesName(const std::string &seriesId) const
|
|
{
|
|
auto it = seriesMap.find(seriesId);
|
|
if (it != seriesMap.end()) {
|
|
return it->second.name;
|
|
}
|
|
return seriesId;
|
|
}
|
|
|
|
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 10-minute window)
|
|
for (int i = 0; i <= 5; i++) {
|
|
double x = marginLeft + (plotWidth * i / 5.0);
|
|
|
|
// Calculate time offset in seconds
|
|
int secondsOffset = (5 - i) * 120; // 10 minutes / 5 = 2 minutes between points
|
|
int minutes = secondsOffset / 60;
|
|
|
|
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 10-minute window)
|
|
for (auto &seriesEntry : seriesMap) {
|
|
SeriesData &series = seriesEntry.second;
|
|
|
|
if (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
|
|
double x = marginLeft + plotWidth * (1.0 - (double)(maxTime - point.timestamp) / MAX_TIME_MS);
|
|
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<double>::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;
|
|
|
|
for (const DataPoint &point : series.points) {
|
|
double x = marginLeft + plotWidth * (1.0 - (double)(maxTime - point.timestamp) / MAX_TIME_MS);
|
|
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<TemperatureChart*>(userData);
|
|
self->hideTooltip();
|
|
}
|