/* This file is part of GNUnet Copyright (C) 2011 GNUnet e.V. GNUnet is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 3, or (at your option) any later version. GNUnet is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with GNUnet; see the file COPYING. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ /** * @file src/statistics/gtk_statistics.c * @brief widget to display statistics * @author Christian Grothoff */ #include "gnunet_gtk.h" #include "gtk_statistics.h" #define MAX_HISTORY 1280 /** * Number of ticks on the x-axis. */ #define XTICKS 4 /** * Number of ticks on the y-axis. */ #define YTICKS 5 /** * Additional distance between text and lines / borders in pixels. */ #define BORDER 10.0 /** * Information about a value we received. */ struct HistoricValue { /** * Time when the value was recorded. */ uint64_t x; /** * A value we recorded. */ uint64_t y; }; /** * Historic information we recorded for a value. */ struct ValueHistory { /** * ID for value updates. */ char *id; /** * Label for the subsystem. */ char *label; /** * Color values (rgb). */ double red, green, blue; /** * Recent values for this number. */ struct HistoricValue history[MAX_HISTORY]; /** * Last offset we wrote to in history. */ unsigned int last_history_offset; /** * Number of valid entries in the history. */ unsigned int history_size; }; struct _GtkStatisticsPrivate { /** * Values we plot. */ struct ValueHistory **values; /** * Size of the 'values' array. */ unsigned int num_values; }; static gboolean gtk_statistics_draw (GtkWidget * widget, cairo_t * cr); static void gtk_statistics_finalize (GObject * object); G_DEFINE_TYPE (GtkStatistics, gtk_statistics, GTK_TYPE_WIDGET) #if GTK_MAJOR_VERSION < 3 static gboolean statistics_expose (GtkWidget * statistics, GdkEventExpose * event) { GtkAllocation alloc; cairo_t *cr; gint x; gint y; gtk_widget_translate_coordinates (statistics, GTK_WIDGET (gtk_widget_get_toplevel (statistics)), 0, 0, &x, &y); gtk_widget_get_allocation (statistics, &alloc); cr = gdk_cairo_create (statistics->window); cairo_translate (cr, x, y); cairo_rectangle (cr, 0, 0, alloc.width, alloc.height); cairo_clip (cr); gtk_statistics_draw (statistics, cr); cairo_destroy (cr); return FALSE; } #endif static void gtk_statistics_class_init (GtkStatisticsClass * class) { GObjectClass *gobject_class; GtkWidgetClass *widget_class; gobject_class = (GObjectClass *) class; widget_class = (GtkWidgetClass *) class; gobject_class->finalize = gtk_statistics_finalize; #if GTK_MAJOR_VERSION >= 3 widget_class->draw = gtk_statistics_draw; #endif g_type_class_add_private (class, sizeof (GtkStatisticsPrivate)); } static void gtk_statistics_init (GtkStatistics * statistics) { GtkStatisticsPrivate *priv; statistics->priv = G_TYPE_INSTANCE_GET_PRIVATE (statistics, GTK_TYPE_STATISTICS, GtkStatisticsPrivate); priv = statistics->priv; #if GTK_MAJOR_VERSION < 3 g_signal_connect (statistics, "expose-event", G_CALLBACK (statistics_expose), statistics); #endif gtk_widget_set_has_window (GTK_WIDGET (statistics), FALSE); priv->values = NULL; priv->num_values = 0; } /** * gtk_statistics_new: * * Creates a new #GtkStatistics widget. * * Returns: the new #GtkStatistics widget. */ GtkWidget * gtk_statistics_new () { GtkStatisticsPrivate *priv; GtkStatistics *statistics; statistics = g_object_new (GTK_TYPE_STATISTICS, NULL); priv = statistics->priv; priv->values = NULL; priv->num_values = 0; return GTK_WIDGET (statistics); } /** * Add another data series to plot by the statistics widget. * * @param statistics widget to modify * @param id identifier for the series * @param label label to use * @param color_name name of the color to use */ void gtk_statistics_add_line (GtkStatistics * statistics, const char *id, const char *label, const char *color_name) { GtkStatisticsPrivate *priv; struct ValueHistory *vh; GdkColor c; g_return_if_fail (GTK_IS_STATISTICS (statistics)); g_return_if_fail (gdk_color_parse (color_name, &c)); priv = statistics->priv; priv->values = g_realloc (priv->values, sizeof (struct ValueHistory *) * (1 + priv->num_values)); priv->values[priv->num_values++] = vh = g_malloc (sizeof (struct ValueHistory)); vh->id = strdup (id); vh->label = strdup (label); vh->red = c.red / 65535.0; vh->green = c.green / 65535.0; vh->blue = c.blue / 65535.0; vh->last_history_offset = 0; vh->history_size = 0; } /** * Add another value to a data series. * * @param statistics widget to update * @param id identifier of the series * @param x new x-value * @param y new y-value */ void gtk_statistics_update_value (GtkStatistics * statistics, const char *id, uint64_t x, uint64_t y) { GtkStatisticsPrivate *priv; GtkWidget *widget; struct ValueHistory *vh; unsigned int i; g_return_if_fail (GTK_IS_STATISTICS (statistics)); priv = statistics->priv; for (i = 0; i < priv->num_values; i++) { vh = priv->values[i]; if (0 != strcmp (id, vh->id)) continue; if (++vh->last_history_offset == MAX_HISTORY) vh->last_history_offset = 0; if (vh->history_size < MAX_HISTORY) vh->history_size++; vh->history[vh->last_history_offset].x = x; vh->history[vh->last_history_offset].y = y; widget = GTK_WIDGET (statistics); if (gtk_widget_is_drawable (widget)) gtk_widget_queue_draw (widget); } } /** * Convert a number to a nice label for the axis. * * @param num number to convert * @param label where to store the string (must be big enough!) */ static void num_to_label (unsigned long long num, char *label) { if (num > 1000LL * 1000 * 1000 * 1000 * 3) sprintf (label, "%llu t", num / 1000LL / 1000LL / 1000LL / 1000LL); else if (num > 1000LL * 1000 * 1000 * 3) sprintf (label, "%llu g", num / 1000LL / 1000LL / 1000LL); else if (num > 1000LL * 1000 * 3) sprintf (label, "%llu m", num / 1000LL / 1000LL); else if (num > 1000LL * 3) sprintf (label, "%llu k", num / 1000LL); else sprintf (label, "%llu", num); } /** * Draw the statistics widget. * * @param widget widget to draw * @param cr drawing context */ static gboolean gtk_statistics_draw (GtkWidget * widget, cairo_t * cr) { GtkStatistics *statistics = GTK_STATISTICS (widget); GtkStatisticsPrivate *priv = statistics->priv; int width; int height; uint64_t xmin; uint64_t xmax; uint64_t ymax; unsigned int i; unsigned int j; struct ValueHistory *vh; struct HistoricValue *hv; char label[64]; cairo_text_extents_t te; cairo_text_extents_t tex_max; cairo_text_extents_t tey_max; double rx; double ry; unsigned int h; GtkAllocation alloc; /* collect basic data */ xmin = UINT64_MAX; xmax = 0; ymax = 0; for (i = 0; i < priv->num_values; i++) { vh = priv->values[i]; for (j = 0; j < vh->history_size; j++) { hv = &vh->history[(vh->last_history_offset - j + MAX_HISTORY) % MAX_HISTORY]; xmin = GNUNET_MIN (hv->x, xmin); xmax = GNUNET_MAX (hv->x, xmax); ymax = GNUNET_MAX (hv->y, ymax); } } /* round to 10x for nicer legends */ while (0 != (ymax % 10)) ymax++; while (0 != (xmax % 10)) xmax++; while (0 != (xmin % 10)) xmin--; gtk_widget_get_allocation (widget, &alloc); width = alloc.width; height = alloc.height; /* fill with black background */ cairo_set_source_rgb (cr, 0.0, 0.0, 0.0); cairo_paint (cr); if ((0 == priv->num_values) || (ymax == 0)) return FALSE; /* done */ /* select font */ cairo_set_source_rgb (cr, 1.0, 1.0, 1.0); cairo_select_font_face (cr, "Georgia", CAIRO_FONT_SLANT_NORMAL, CAIRO_FONT_WEIGHT_BOLD); cairo_set_font_size (cr, GNUNET_MIN (20, height / (2 * XTICKS))); /* find out needed space for axis labels */ tex_max.width = 0; tex_max.height = 0; for (i = 0; i < XTICKS; i++) { num_to_label ((unsigned long long) (xmin + (xmax - xmin) * ((uint64_t) i) / (XTICKS - 1)), label); cairo_text_extents (cr, label, &te); tex_max.width = GNUNET_MAX (te.width, tex_max.width); tex_max.height = GNUNET_MAX (te.height, tex_max.height); } tey_max.width = 0; tey_max.height = 0; for (i = 0; i < YTICKS; i++) { num_to_label ((unsigned long long) ymax * ((uint64_t) i) / (YTICKS - 1), label); cairo_text_extents (cr, label, &te); tey_max.width = GNUNET_MAX (te.width, tey_max.width); tey_max.height = GNUNET_MAX (te.height, tey_max.height); } /* draw y-axis labels */ for (i = 0; i < YTICKS; i++) { num_to_label ((unsigned long long) ymax * ((uint64_t) i) / (YTICKS - 1), label); cairo_text_extents (cr, label, &te); cairo_move_to (cr, BORDER + tey_max.width - te.width, BORDER + tey_max.height + (YTICKS - i - 1) * (height - 2.0 * BORDER - tey_max.height - tex_max.height) / (double) (YTICKS - 1)); cairo_show_text (cr, label); cairo_move_to (cr, 2.0 * BORDER + tey_max.width, BORDER + tey_max.height / 2.0 + (YTICKS - i - 1) * (height - 2.0 * BORDER - tex_max.height - tey_max.height) / (double) (YTICKS - 1)); cairo_line_to (cr, 2.0 * BORDER + tey_max.width + BORDER / 2.0, BORDER + tey_max.height / 2.0 + (YTICKS - i - 1) * (height - 2.0 * BORDER - tex_max.height - tey_max.height) / (double) (YTICKS - 1)); cairo_stroke (cr); } /* draw x-axis labels */ for (i = 0; i < XTICKS; i++) { num_to_label ((unsigned long long) (xmin + (xmax - xmin) * ((uint64_t) i) / (XTICKS - 1)), label); cairo_text_extents (cr, label, &te); if (i != 0) { cairo_move_to (cr, 2.0 * BORDER + tey_max.width + (width - tey_max.width - tex_max.width / 2.0 - 3.0 * BORDER) * i / (XTICKS - 1.0) - te.width / 2.0, height - BORDER / 2.0 - tex_max.height / 2.0); cairo_show_text (cr, label); } cairo_move_to (cr, 2.0 * BORDER + tey_max.width + (width - tey_max.width - tex_max.width / 2.0 - 3.0 * BORDER) * i / (XTICKS - 1.0), height - BORDER - tey_max.height / 2.0 - tex_max.height); cairo_line_to (cr, 2.0 * BORDER + tey_max.width + (width - tey_max.width - tex_max.width / 2.0 - 3.0 * BORDER) * i / (XTICKS - 1.0), height - BORDER - tey_max.height / 2.0 - tex_max.height - BORDER / 2.0); cairo_stroke (cr); } /* plot border */ cairo_set_line_width (cr, 1.0); cairo_rectangle (cr, tey_max.width + BORDER * 2.0, BORDER + tey_max.height / 2.0, width - BORDER * 3.0 - tey_max.width - tex_max.width / 2.0, height - BORDER * 2.0 - tey_max.height - tex_max.height); cairo_stroke (cr); /* finally, plot lines */ cairo_set_line_width (cr, 2.0); cairo_set_font_size (cr, GNUNET_MIN (20, (height - 3.0 * BORDER - tex_max.height - tey_max.height / 2) / (priv->num_values + 1))); h = 2.0 * BORDER + tey_max.height / 2; for (i = 0; i < priv->num_values; i++) { vh = priv->values[i]; cairo_set_source_rgb (cr, vh->red, vh->green, vh->blue); cairo_text_extents (cr, vh->label, &te); h += te.height / 2; cairo_move_to (cr, 3.0 * BORDER + tey_max.width, h); h += te.height / 2 + 1.0; cairo_show_text (cr, vh->label); if (xmax == xmin) { hv = &vh->history[vh->last_history_offset % MAX_HISTORY]; ry = hv->y / (double) ymax; ry = BORDER + tex_max.height / 2.0 + (1.0 - ry) * (height - BORDER * 2.0 - tey_max.height - tex_max.height); /* if y-values are small, offset y-values a bit to allow overlapping curves to still show up */ if (ymax < height / (priv->num_values * 4)) ry += priv->num_values * 2 - (4 * i); cairo_move_to (cr, width - BORDER - tex_max.width / 2.0, ry); cairo_line_to (cr, rx = tey_max.width + BORDER * 2.0, ry); cairo_stroke (cr); continue; } for (j = 0; j < vh->history_size; j++) { hv = &vh->history[(vh->last_history_offset - j + MAX_HISTORY) % MAX_HISTORY]; rx = (hv->x - xmin) / (double) (xmax - xmin); ry = hv->y / (double) ymax; rx = tey_max.width + BORDER * 2.0 + (rx * (width - BORDER * 3.0 - tey_max.width - tex_max.width / 2.0)); ry = BORDER + tex_max.height / 2.0 + (1.0 - ry) * (height - BORDER * 2.0 - tey_max.height - tex_max.height); /* if y-values are small, offset y-values a bit to allow overlapping curves to still show up */ if (ymax < height / (priv->num_values * 4)) ry += priv->num_values * 2 - (4 * i); if (j == 0) cairo_move_to (cr, width - BORDER - tex_max.width / 2.0, ry); cairo_line_to (cr, rx, ry); } cairo_stroke (cr); } return FALSE; } /** * Free memory used by statistics object. * * @param object object to release */ static void gtk_statistics_finalize (GObject * object) { GtkStatistics *label = GTK_STATISTICS (object); GtkStatisticsPrivate *priv = label->priv; unsigned int i; for (i = 0; i < priv->num_values; i++) { g_free (priv->values[i]->label); g_free (priv->values[i]->id); g_free (priv->values[i]); } g_free (priv->values); G_OBJECT_CLASS (gtk_statistics_parent_class)->finalize (object); } /* end of gtk_statistics.c */