*
* Copyright 2025 Adam Masciola
*
* This program 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 of the License, or
* (at your option) any later version.
*
* This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
*
* SPDX-License-Identifier: GPL-3.0-or-later
*/
#include "config.h"
#include <adwaita.h>
#include "bz-data-graph.h"
#include "bz-data-point.h"
#include <math.h>
#define LABEL_MARGIN 75.0
#define LABEL_MARGIN_RIGHT 35.0
#define TICK_LENGTH 5.0
struct _BzDataGraph
{
GtkWidget parent_instance;
GListModel *model;
char *independent_axis_label;
char *dependent_axis_label;
char *tooltip_prefix;
int independent_decimals;
int dependent_decimals;
double transition_progress;
double rounded_axis_max;
AdwStyleManager *style_manager;
GskPath *path;
GskPathMeasure *path_measure;
GskRenderNode *other;
GskRenderNode *graph;
GskRenderNode *overlay;
gboolean wants_animate_open;
GtkEventController *motion;
double motion_x;
double motion_y;
GtkGesture *gesture;
GtkWidget *tooltip_box;
GtkWidget *tooltip_label1;
GtkWidget *tooltip_prefix_label;
GtkWidget *tooltip_label2;
int allocated_width;
int allocated_height;
};
G_DEFINE_FINAL_TYPE (BzDataGraph, bz_data_graph, GTK_TYPE_WIDGET)
enum
{
PROP_0,
PROP_MODEL,
PROP_INDEPENDENT_AXIS_LABEL,
PROP_DEPENDENT_AXIS_LABEL,
PROP_TOOLTIP_PREFIX,
PROP_INDEPENDENT_DECIMALS,
PROP_DEPENDENT_DECIMALS,
PROP_TRANSITION_PROGRESS,
LAST_PROP
};
static GParamSpec *props[LAST_PROP] = { 0 };
static void
items_changed (GListModel *model,
guint position,
guint removed,
guint added,
BzDataGraph *self);
static void
refresh_path (BzDataGraph *self,
double width,
double height);
static double
calculate_axis_tick_value (double value, gboolean round_up);
static void
on_style_changed (BzDataGraph *self,
GParamSpec *pspec,
AdwStyleManager *style_manager);
static void
refresh_graph (BzDataGraph *self);
static void
refresh_overlay (BzDataGraph *self);
static void
bz_data_graph_dispose (GObject *object)
{
BzDataGraph *self = BZ_DATA_GRAPH (object);
if (self->model != NULL)
g_signal_handlers_disconnect_by_func (
self->model, items_changed, self);
g_signal_handlers_disconnect_by_func (self->style_manager,
on_style_changed,
self);
g_clear_object (&self->model);
g_clear_pointer (&self->independent_axis_label, g_free);
g_clear_pointer (&self->dependent_axis_label, g_free);
g_clear_pointer (&self->tooltip_prefix, g_free);
g_clear_pointer (&self->path, gsk_path_unref);
g_clear_pointer (&self->path_measure, gsk_path_measure_unref);
g_clear_pointer (&self->other, gsk_render_node_unref);
g_clear_pointer (&self->graph, gsk_render_node_unref);
g_clear_pointer (&self->overlay, gsk_render_node_unref);
if (self->tooltip_box != NULL)
gtk_widget_unparent (self->tooltip_box);
G_OBJECT_CLASS (bz_data_graph_parent_class)->dispose (object);
}
static void
bz_data_graph_get_property (GObject *object,
guint prop_id,
GValue *value,
GParamSpec *pspec)
{
BzDataGraph *self = BZ_DATA_GRAPH (object);
switch (prop_id)
{
case PROP_MODEL:
g_value_set_object (value, bz_data_graph_get_model (self));
break;
case PROP_INDEPENDENT_AXIS_LABEL:
g_value_set_string (value, bz_data_graph_get_independent_axis_label (self));
break;
case PROP_DEPENDENT_AXIS_LABEL:
g_value_set_string (value, bz_data_graph_get_dependent_axis_label (self));
break;
case PROP_TOOLTIP_PREFIX:
g_value_set_string (value, bz_data_graph_get_tooltip_prefix (self));
break;
case PROP_INDEPENDENT_DECIMALS:
g_value_set_int (value, bz_data_graph_get_independent_decimals (self));
break;
case PROP_DEPENDENT_DECIMALS:
g_value_set_int (value, bz_data_graph_get_dependent_decimals (self));
break;
case PROP_TRANSITION_PROGRESS:
g_value_set_double (value, bz_data_graph_get_transition_progress (self));
break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
}
}
static void
bz_data_graph_set_property (GObject *object,
guint prop_id,
const GValue *value,
GParamSpec *pspec)
{
BzDataGraph *self = BZ_DATA_GRAPH (object);
switch (prop_id)
{
case PROP_MODEL:
bz_data_graph_set_model (self, g_value_get_object (value));
break;
case PROP_INDEPENDENT_AXIS_LABEL:
bz_data_graph_set_independent_axis_label (self, g_value_get_string (value));
break;
case PROP_DEPENDENT_AXIS_LABEL:
bz_data_graph_set_dependent_axis_label (self, g_value_get_string (value));
break;
case PROP_TOOLTIP_PREFIX:
bz_data_graph_set_tooltip_prefix (self, g_value_get_string (value));
break;
case PROP_INDEPENDENT_DECIMALS:
bz_data_graph_set_independent_decimals (self, g_value_get_int (value));
break;
case PROP_DEPENDENT_DECIMALS:
bz_data_graph_set_dependent_decimals (self, g_value_get_int (value));
break;
case PROP_TRANSITION_PROGRESS:
bz_data_graph_set_transition_progress (self, g_value_get_double (value));
break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
}
}
static void
bz_data_graph_size_allocate (GtkWidget *widget,
int width,
int height,
int baseline)
{
BzDataGraph *self = BZ_DATA_GRAPH (widget);
GtkRequisition natural_size = { 0 };
graphene_point_t card_position = { 0 };
if (width != self->allocated_width ||
height != self->allocated_height)
{
refresh_path (
self,
(double) width - LABEL_MARGIN - LABEL_MARGIN_RIGHT,
(double) height - LABEL_MARGIN);
refresh_graph (self);
refresh_overlay (self);
self->allocated_width = width;
self->allocated_height = height;
}
gtk_widget_get_preferred_size (self->tooltip_box, NULL, &natural_size);
if (self->motion_x > width / 2.0)
card_position.x = self->motion_x - natural_size.width - 10.0;
else
card_position.x = self->motion_x + 10.0;
card_position.y = self->motion_y + 10.0;
gtk_widget_allocate (
self->tooltip_box,
natural_size.width,
natural_size.height,
baseline,
gsk_transform_translate (NULL, &card_position));
gtk_widget_queue_draw (widget);
}
static void
on_style_changed (BzDataGraph *self,
GParamSpec *pspec,
AdwStyleManager *style_manager)
{
refresh_graph (self);
gtk_widget_queue_draw (GTK_WIDGET (self));
}
static void
bz_data_graph_snapshot (GtkWidget *widget,
GtkSnapshot *snapshot)
{
BzDataGraph *self = BZ_DATA_GRAPH (widget);
if (self->path == NULL)
return;
if (self->graph != NULL)
gtk_snapshot_append_node (snapshot, self->graph);
if (self->other != NULL)
{
GdkRGBA widget_color = { 0 };
gtk_widget_get_color (widget, &widget_color);
if (self->other != NULL)
{
graphene_rect_t bounds = { 0 };
gsk_render_node_get_bounds (self->other, &bounds);
gtk_snapshot_push_mask (snapshot, GSK_MASK_MODE_ALPHA);
gtk_snapshot_append_node (snapshot, self->other);
gtk_snapshot_pop (snapshot);
gtk_snapshot_append_color (snapshot, &widget_color, &bounds);
gtk_snapshot_pop (snapshot);
}
}
if (self->overlay != NULL)
{
gtk_snapshot_append_node (snapshot, self->overlay);
gtk_widget_snapshot_child (GTK_WIDGET (self), self->tooltip_box, snapshot);
}
if (self->wants_animate_open)
{
AdwAnimationTarget *transition_target = NULL;
AdwSpringParams *transition_spring = NULL;
g_autoptr (AdwAnimation) transition = NULL;
self->wants_animate_open = FALSE;
transition_target = adw_property_animation_target_new (G_OBJECT (self), "transition-progress");
transition_spring = adw_spring_params_new (1.0, 1.0, 80.0);
transition = adw_spring_animation_new (GTK_WIDGET (self), 0.0, 1.0, transition_spring, transition_target);
adw_spring_animation_set_epsilon (ADW_SPRING_ANIMATION (transition), 0.000001);
adw_animation_play (transition);
}
}
static void
bz_data_graph_class_init (BzDataGraphClass *klass)
{
GObjectClass *object_class = G_OBJECT_CLASS (klass);
GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
object_class->dispose = bz_data_graph_dispose;
object_class->get_property = bz_data_graph_get_property;
object_class->set_property = bz_data_graph_set_property;
props[PROP_MODEL] =
g_param_spec_object (
"model",
NULL, NULL,
G_TYPE_LIST_MODEL,
G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY);
props[PROP_INDEPENDENT_AXIS_LABEL] =
g_param_spec_string (
"independent-axis-label",
NULL, NULL, NULL,
G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY);
props[PROP_DEPENDENT_AXIS_LABEL] =
g_param_spec_string (
"dependent-axis-label",
NULL, NULL, NULL,
G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY);
props[PROP_TOOLTIP_PREFIX] =
g_param_spec_string (
"tooltip-prefix",
NULL, NULL, NULL,
G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY);
props[PROP_INDEPENDENT_DECIMALS] =
g_param_spec_int (
"independent-decimals",
NULL, NULL,
-1, 4, (int) 0,
G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY);
props[PROP_DEPENDENT_DECIMALS] =
g_param_spec_int (
"dependent-decimals",
NULL, NULL,
-1, 4, (int) 0,
G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY);
props[PROP_TRANSITION_PROGRESS] =
g_param_spec_double (
"transition-progress",
NULL, NULL,
0.0, G_MAXDOUBLE, 0.0,
G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY);
g_object_class_install_properties (object_class, LAST_PROP, props);
widget_class->size_allocate = bz_data_graph_size_allocate;
widget_class->snapshot = bz_data_graph_snapshot;
}
static void
update_cursor (BzDataGraph *self,
gdouble x,
gdouble y)
{
double widget_width = gtk_widget_get_width (GTK_WIDGET (self));
double widget_height = gtk_widget_get_height (GTK_WIDGET (self));
if (x >= LABEL_MARGIN &&
y >= 0.0 &&
x < widget_width - LABEL_MARGIN_RIGHT &&
y < widget_height - LABEL_MARGIN)
gtk_widget_set_cursor_from_name (GTK_WIDGET (self), "crosshair");
else
gtk_widget_set_cursor (GTK_WIDGET (self), NULL);
refresh_overlay (self);
gtk_widget_queue_allocate (GTK_WIDGET (self));
}
static void
motion_enter (BzDataGraph *self,
gdouble x,
gdouble y,
GtkEventControllerMotion *controller)
{
self->motion_x = x;
self->motion_y = y;
update_cursor (self, x, y);
}
static void
motion_event (BzDataGraph *self,
gdouble x,
gdouble y,
GtkEventControllerMotion *controller)
{
self->motion_x = x;
self->motion_y = y;
update_cursor (self, x, y);
}
static void
motion_leave (BzDataGraph *self,
GtkEventControllerMotion *controller)
{
self->motion_x = -1.0;
self->motion_y = -1.0;
update_cursor (self, -1.0, -1.0);
}
static void
gesture_begin (BzDataGraph *self,
double start_x,
double start_y,
GtkGestureDrag *gesture)
{
self->motion_x = start_x;
self->motion_y = start_y;
gtk_widget_queue_draw (GTK_WIDGET (self));
}
static void
gesture_update (BzDataGraph *self,
double offset_x,
double offset_y,
GtkGestureDrag *gesture)
{
double start_x, start_y;
gtk_gesture_drag_get_start_point (gesture, &start_x, &start_y);
self->motion_x = start_x + offset_x;
self->motion_y = start_y + offset_y;
gtk_widget_queue_draw (GTK_WIDGET (self));
}
static void
gesture_end (BzDataGraph *self,
double offset_x,
double offset_y,
GtkGestureDrag *gesture)
{
gtk_widget_queue_draw (GTK_WIDGET (self));
}
static void
bz_data_graph_init (BzDataGraph *self)
{
GtkWidget *inner_box = NULL;
GtkWidget *label2_box = NULL;
GtkWidget *icon_image = NULL;
self->style_manager = adw_style_manager_get_default ();
g_signal_connect_swapped (self->style_manager, "notify::dark",
G_CALLBACK (on_style_changed), self);
g_signal_connect_swapped (self->style_manager, "notify::accent-color",
G_CALLBACK (on_style_changed), self);
self->motion = gtk_event_controller_motion_new ();
g_signal_connect_swapped (self->motion, "enter", G_CALLBACK (motion_enter), self);
g_signal_connect_swapped (self->motion, "motion", G_CALLBACK (motion_event), self);
g_signal_connect_swapped (self->motion, "leave", G_CALLBACK (motion_leave), self);
gtk_widget_add_controller (GTK_WIDGET (self), self->motion);
self->gesture = gtk_gesture_drag_new ();
gtk_gesture_single_set_touch_only (GTK_GESTURE_SINGLE (self->gesture), TRUE);
g_signal_connect_swapped (self->gesture, "drag-begin", G_CALLBACK (gesture_begin), self);
g_signal_connect_swapped (self->gesture, "drag-update", G_CALLBACK (gesture_update), self);
g_signal_connect_swapped (self->gesture, "drag-end", G_CALLBACK (gesture_end), self);
gtk_widget_add_controller (GTK_WIDGET (self), GTK_EVENT_CONTROLLER (self->gesture));
self->motion_x = -1.0;
self->motion_y = -1.0;
self->rounded_axis_max = 0.0;
self->tooltip_box = gtk_box_new (GTK_ORIENTATION_VERTICAL, 0);
gtk_widget_add_css_class (self->tooltip_box, "card");
gtk_widget_add_css_class (self->tooltip_box, "floating-tooltip");
inner_box = gtk_box_new (GTK_ORIENTATION_VERTICAL, 4);
gtk_widget_set_margin_start (inner_box, 12);
gtk_widget_set_margin_end (inner_box, 12);
gtk_widget_set_margin_top (inner_box, 12);
gtk_widget_set_margin_bottom (inner_box, 12);
self->tooltip_label1 = gtk_label_new ("");
gtk_widget_add_css_class (self->tooltip_label1, "heading");
gtk_label_set_xalign (GTK_LABEL (self->tooltip_label1), 0.0);
gtk_box_append (GTK_BOX (inner_box), self->tooltip_label1);
label2_box = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 6);
icon_image = gtk_image_new_from_icon_name ("square-filled-symbolic");
gtk_widget_add_css_class (icon_image, "accent");
gtk_box_append (GTK_BOX (label2_box), icon_image);
self->tooltip_prefix_label = gtk_label_new ("");
gtk_widget_add_css_class (self->tooltip_prefix_label, "body");
gtk_widget_add_css_class (self->tooltip_prefix_label, "dim-label");
gtk_label_set_xalign (GTK_LABEL (self->tooltip_prefix_label), 0.0);
gtk_box_append (GTK_BOX (label2_box), self->tooltip_prefix_label);
self->tooltip_label2 = gtk_label_new ("");
gtk_widget_add_css_class (self->tooltip_label2, "monospace");
gtk_widget_add_css_class (self->tooltip_label2, "dimmed");
gtk_label_set_xalign (GTK_LABEL (self->tooltip_label2), 0.0);
gtk_box_append (GTK_BOX (label2_box), self->tooltip_label2);
gtk_box_append (GTK_BOX (inner_box), label2_box);
gtk_box_append (GTK_BOX (self->tooltip_box), inner_box);
gtk_widget_set_parent (self->tooltip_box, GTK_WIDGET (self));
}
GtkWidget *
bz_data_graph_new (void)
{
return g_object_new (BZ_TYPE_DATA_GRAPH, NULL);
}
GListModel *
bz_data_graph_get_model (BzDataGraph *self)
{
g_return_val_if_fail (BZ_IS_DATA_GRAPH (self), NULL);
return self->model;
}
const char *
bz_data_graph_get_independent_axis_label (BzDataGraph *self)
{
g_return_val_if_fail (BZ_IS_DATA_GRAPH (self), NULL);
return self->independent_axis_label;
}
const char *
bz_data_graph_get_dependent_axis_label (BzDataGraph *self)
{
g_return_val_if_fail (BZ_IS_DATA_GRAPH (self), NULL);
return self->dependent_axis_label;
}
const char *
bz_data_graph_get_tooltip_prefix (BzDataGraph *self)
{
g_return_val_if_fail (BZ_IS_DATA_GRAPH (self), NULL);
return self->tooltip_prefix;
}
int
bz_data_graph_get_independent_decimals (BzDataGraph *self)
{
g_return_val_if_fail (BZ_IS_DATA_GRAPH (self), 0);
return self->independent_decimals;
}
int
bz_data_graph_get_dependent_decimals (BzDataGraph *self)
{
g_return_val_if_fail (BZ_IS_DATA_GRAPH (self), 0);
return self->dependent_decimals;
}
double
bz_data_graph_get_transition_progress (BzDataGraph *self)
{
g_return_val_if_fail (BZ_IS_DATA_GRAPH (self), 0.0);
return self->transition_progress;
}
void
bz_data_graph_set_model (BzDataGraph *self,
GListModel *model)
{
g_return_if_fail (BZ_IS_DATA_GRAPH (self));
g_return_if_fail (model == NULL || G_IS_LIST_MODEL (model));
if (self->model != NULL)
g_signal_handlers_disconnect_by_func (
self->model, items_changed, self);
g_clear_object (&self->model);
if (model != NULL)
self->model = g_object_ref (model);
self->allocated_width = -1;
self->allocated_height = -1;
gtk_widget_queue_allocate (GTK_WIDGET (self));
g_object_notify_by_pspec (G_OBJECT (self), props[PROP_MODEL]);
}
void
bz_data_graph_set_independent_axis_label (BzDataGraph *self,
const char *independent_axis_label)
{
g_return_if_fail (BZ_IS_DATA_GRAPH (self));
g_clear_pointer (&self->independent_axis_label, g_free);
if (independent_axis_label != NULL)
self->independent_axis_label = g_strdup (independent_axis_label);
gtk_widget_queue_draw (GTK_WIDGET (self));
g_object_notify_by_pspec (G_OBJECT (self), props[PROP_INDEPENDENT_AXIS_LABEL]);
}
void
bz_data_graph_set_dependent_axis_label (BzDataGraph *self,
const char *dependent_axis_label)
{
g_return_if_fail (BZ_IS_DATA_GRAPH (self));
g_clear_pointer (&self->dependent_axis_label, g_free);
if (dependent_axis_label != NULL)
self->dependent_axis_label = g_strdup (dependent_axis_label);
gtk_widget_queue_draw (GTK_WIDGET (self));
g_object_notify_by_pspec (G_OBJECT (self), props[PROP_DEPENDENT_AXIS_LABEL]);
}
void
bz_data_graph_set_tooltip_prefix (BzDataGraph *self,
const char *tooltip_prefix)
{
g_return_if_fail (BZ_IS_DATA_GRAPH (self));
g_clear_pointer (&self->tooltip_prefix, g_free);
if (tooltip_prefix != NULL)
self->tooltip_prefix = g_strdup (tooltip_prefix);
gtk_widget_queue_draw (GTK_WIDGET (self));
g_object_notify_by_pspec (G_OBJECT (self), props[PROP_TOOLTIP_PREFIX]);
}
void
bz_data_graph_set_independent_decimals (BzDataGraph *self,
int independent_decimals)
{
g_return_if_fail (BZ_IS_DATA_GRAPH (self));
self->independent_decimals = CLAMP (independent_decimals, -1, 4);
gtk_widget_queue_allocate (GTK_WIDGET (self));
g_object_notify_by_pspec (G_OBJECT (self), props[PROP_INDEPENDENT_DECIMALS]);
}
void
bz_data_graph_set_dependent_decimals (BzDataGraph *self,
int dependent_decimals)
{
g_return_if_fail (BZ_IS_DATA_GRAPH (self));
self->dependent_decimals = CLAMP (dependent_decimals, -1, 4);
gtk_widget_queue_allocate (GTK_WIDGET (self));
g_object_notify_by_pspec (G_OBJECT (self), props[PROP_DEPENDENT_DECIMALS]);
}
void
bz_data_graph_set_transition_progress (BzDataGraph *self, double transition_progress)
{
g_return_if_fail (BZ_IS_DATA_GRAPH (self));
self->transition_progress = transition_progress;
refresh_graph (self);
gtk_widget_queue_draw (GTK_WIDGET (self));
g_object_notify_by_pspec (G_OBJECT (self), props[PROP_TRANSITION_PROGRESS]);
}
void
bz_data_graph_animate_open (BzDataGraph *self)
{
g_return_if_fail (BZ_IS_DATA_GRAPH (self));
self->wants_animate_open = TRUE;
gtk_widget_queue_draw (GTK_WIDGET (self));
}
static void
items_changed (GListModel *model,
guint position,
guint removed,
guint added,
BzDataGraph *self)
{
gtk_widget_queue_allocate (GTK_WIDGET (self));
}
static double
calculate_axis_tick_value (double value, gboolean round_up)
{
double exponent = 0.0;
double fraction = 0.0;
double rounded_axis_fraction = 0.0;
exponent = floor (log10 (value));
fraction = value / pow (10, exponent);
if (round_up)
{
if (fraction <= 1.0)
rounded_axis_fraction = 1.0;
else if (fraction <= 2.0)
rounded_axis_fraction = 2.0;
else if (fraction <= 5.0)
rounded_axis_fraction = 5.0;
else
rounded_axis_fraction = 10.0;
}
else if (fraction < 1.5)
rounded_axis_fraction = 1.0;
else if (fraction < 3.0)
rounded_axis_fraction = 2.0;
else if (fraction < 7.0)
rounded_axis_fraction = 5.0;
else
rounded_axis_fraction = 10.0;
return rounded_axis_fraction * pow (10, exponent);
}
static void
refresh_path (BzDataGraph *self,
double width,
double height)
{
guint n_items = 0;
double min_independent = 0.0;
double max_independent = 0.0;
double max_dependent = 0.0;
PangoContext *pango = NULL;
PangoFontMetrics *metrics = NULL;
double font_height = 0.0;
int independent_label_step = 0;
g_autoptr (GskPathBuilder) curve_builder = NULL;
g_autoptr (GtkSnapshot) snapshot = NULL;
g_autoptr (GskPathBuilder) grid_builder = NULL;
g_autoptr (GskPath) grid = NULL;
g_autoptr (GskStroke) grid_stroke = NULL;
double rounded_axis_max = 0.0;
double tick_spacing = 0.0;
int num_ticks = 0;
g_clear_pointer (&self->path, gsk_path_unref);
g_clear_pointer (&self->path_measure, gsk_path_measure_unref);
g_clear_pointer (&self->other, gsk_render_node_unref);
if (self->model == NULL)
return;
if (width < LABEL_MARGIN || height < LABEL_MARGIN)
return;
n_items = g_list_model_get_n_items (self->model);
if (n_items <= 1)
return;
for (guint i = 0; i < n_items; i++)
{
g_autoptr (BzDataPoint) point = NULL;
double independent = 0.0;
double dependent = 0.0;
point = g_list_model_get_item (self->model, i);
independent = bz_data_point_get_independent (point);
dependent = bz_data_point_get_dependent (point);
if (i == 0)
{
min_independent = independent;
max_independent = independent;
max_dependent = dependent;
}
else
{
min_independent = MIN (independent, min_independent);
max_independent = MAX (independent, max_independent);
max_dependent = MAX (dependent, max_dependent);
}
}
rounded_axis_max = calculate_axis_tick_value (max_dependent, TRUE);
pango = gtk_widget_get_pango_context (GTK_WIDGET (self));
metrics = pango_context_get_metrics (pango, NULL, NULL);
font_height = (double) (int) PANGO_PIXELS_CEIL (pango_font_metrics_get_height (metrics));
g_clear_pointer (&metrics, pango_font_metrics_unref);
num_ticks = MIN (5, floor (height / (font_height + 25.0)));
if (num_ticks < 2)
num_ticks = 2;
tick_spacing = calculate_axis_tick_value (rounded_axis_max / (double) num_ticks, FALSE);
if (tick_spacing == 0.0)
tick_spacing = 1.0;
rounded_axis_max = ceil (max_dependent / tick_spacing) * tick_spacing;
self->rounded_axis_max = rounded_axis_max;
independent_label_step = MAX (1, n_items / MAX (1, floor (width / MAX (font_height + 10.0, LABEL_MARGIN)))) * 1.5;
curve_builder = gsk_path_builder_new ();
snapshot = gtk_snapshot_new ();
grid_builder = gsk_path_builder_new ();
gtk_snapshot_save (snapshot);
gtk_snapshot_translate (snapshot, &GRAPHENE_POINT_INIT (LABEL_MARGIN, 0.0));
for (guint i = 0; i < n_items; i++)
{
g_autoptr (BzDataPoint) point = NULL;
double independent = 0.0;
double dependent = 0.0;
double x = 0.0;
double y = 0.0;
point = g_list_model_get_item (self->model, i);
independent = bz_data_point_get_independent (point);
dependent = bz_data_point_get_dependent (point);
x = (independent - min_independent) / (max_independent - min_independent) * width;
y = (1.0 - dependent / rounded_axis_max) * height;
if (i == 0)
gsk_path_builder_move_to (curve_builder, x, y);
else
gsk_path_builder_line_to (curve_builder, x, y);
if (i % independent_label_step == 0)
{
const char *label = NULL;
char buf[32] = { 0 };
g_autoptr (PangoLayout) layout = NULL;
PangoRectangle extents;
label = bz_data_point_get_label (point);
if (label == NULL)
{
switch (self->independent_decimals)
{
case 0:
g_snprintf (buf, sizeof (buf), "%d", (int) round (independent));
break;
case 1:
g_snprintf (buf, sizeof (buf), "%.1f", independent);
break;
case 2:
g_snprintf (buf, sizeof (buf), "%.2f", independent);
break;
case 3:
g_snprintf (buf, sizeof (buf), "%.3f", independent);
break;
default:
g_snprintf (buf, sizeof (buf), "%f", independent);
break;
}
label = buf;
}
layout = pango_layout_new (pango);
pango_layout_set_text (layout, label, -1);
pango_layout_get_pixel_extents (layout, NULL, &extents);
gtk_snapshot_save (snapshot);
gtk_snapshot_translate (snapshot, &GRAPHENE_POINT_INIT (x, height + LABEL_MARGIN / 10.0));
gtk_snapshot_rotate (snapshot, -LABEL_MARGIN_RIGHT);
gtk_snapshot_translate (snapshot, &GRAPHENE_POINT_INIT (-extents.width, 0));
gtk_snapshot_append_layout (snapshot, layout, &(GdkRGBA){ 1.0, 1.0, 1.0, 1.0 });
gtk_snapshot_restore (snapshot);
gsk_path_builder_move_to (grid_builder, x, height);
gsk_path_builder_line_to (grid_builder, x, height + TICK_LENGTH);
}
}
gsk_path_builder_move_to (grid_builder, 0.0, height);
gsk_path_builder_line_to (grid_builder, width, height);
gtk_snapshot_save (snapshot);
gtk_snapshot_translate (snapshot, &GRAPHENE_POINT_INIT (-LABEL_MARGIN * 0.75, -font_height / 2.0));
for (double value = 0.0; value <= rounded_axis_max; value += tick_spacing)
{
char buf[32] = { 0 };
g_autoptr (PangoLayout) layout = NULL;
double y_pos = (1.0 - value / rounded_axis_max) * height;
switch (self->dependent_decimals)
{
case 0:
g_snprintf (buf, sizeof (buf), "%d", (int) round (value));
break;
case 1:
g_snprintf (buf, sizeof (buf), "%.1f", value);
break;
case 2:
g_snprintf (buf, sizeof (buf), "%.2f", value);
break;
case 3:
g_snprintf (buf, sizeof (buf), "%.3f", value);
break;
default:
g_snprintf (buf, sizeof (buf), "%f", value);
break;
}
layout = pango_layout_new (pango);
pango_layout_set_text (layout, buf, -1);
gtk_snapshot_save (snapshot);
gtk_snapshot_translate (snapshot, &GRAPHENE_POINT_INIT (0, y_pos));
gtk_snapshot_append_layout (snapshot, layout, &(GdkRGBA){ 1.0, 1.0, 1.0, 1.0 });
gtk_snapshot_restore (snapshot);
gsk_path_builder_move_to (grid_builder, -TICK_LENGTH, y_pos);
gsk_path_builder_line_to (grid_builder, 0.0, y_pos);
}
gtk_snapshot_restore (snapshot);
gsk_path_builder_move_to (grid_builder, 0.0, 0.0);
gsk_path_builder_line_to (grid_builder, 0.0, height);
grid = gsk_path_builder_to_path (grid_builder);
grid_stroke = gsk_stroke_new (1.0);
gtk_snapshot_append_stroke (snapshot, grid, grid_stroke, &(GdkRGBA){ 1.0, 1.0, 1.0, 1.0 });
gtk_snapshot_restore (snapshot);
self->path = gsk_path_builder_to_path (curve_builder);
self->path_measure = gsk_path_measure_new (self->path);
self->other = gtk_snapshot_to_node (snapshot);
}
static void
refresh_graph (BzDataGraph *self)
{
double widget_width = 0.0;
double widget_height = 0.0;
AdwStyleManager *style_manager = NULL;
g_autoptr (GdkRGBA) accent_color = NULL;
GdkRGBA widget_color = { 0 };
g_autoptr (GskPath) transitioning = NULL;
g_autoptr (GskStroke) stroke = NULL;
g_autoptr (GtkSnapshot) snapshot = NULL;
if (self->path == NULL)
{
g_clear_pointer (&self->graph, gsk_render_node_unref);
return;
}
widget_width = gtk_widget_get_width (GTK_WIDGET (self));
(void) widget_width;
widget_height = gtk_widget_get_height (GTK_WIDGET (self));
style_manager = adw_style_manager_get_default ();
accent_color = adw_style_manager_get_accent_color_rgba (style_manager);
gtk_widget_get_color (GTK_WIDGET (self), &widget_color);
if (self->transition_progress > 0.0 && self->transition_progress < 1.0)
{
GskPathPoint point0 = { 0 };
double path_distance = 0.0;
GskPathPoint point1 = { 0 };
g_autoptr (GskPathBuilder) builder = NULL;
gsk_path_get_start_point (self->path, &point0);
path_distance = gsk_path_measure_get_length (self->path_measure) * self->transition_progress;
gsk_path_measure_get_point (self->path_measure, path_distance, &point1);
builder = gsk_path_builder_new ();
gsk_path_builder_add_segment (builder, self->path, &point0, &point1);
transitioning = gsk_path_builder_to_path (builder);
}
stroke = gsk_stroke_new (3.0);
gsk_stroke_set_line_cap (stroke, GSK_LINE_CAP_ROUND);
snapshot = gtk_snapshot_new ();
gtk_snapshot_save (snapshot);
gtk_snapshot_translate (snapshot, &GRAPHENE_POINT_INIT (LABEL_MARGIN, 0.0));
if (self->transition_progress > 0.0)
{
g_autoptr (GskPathBuilder) builder = NULL;
GskPathPoint point0 = { 0 };
GskPathPoint point1 = { 0 };
graphene_point_t start_position = { 0 };
graphene_point_t end_position = { 0 };
g_autoptr (GskPath) path = NULL;
const GskColorStop gradient_stops[] = {
{
0.0,
{
.red = accent_color->red,
.green = accent_color->green,
.blue = accent_color->blue,
.alpha = 0.75,
},
},
{
1.0,
{
.red = accent_color->red,
.green = accent_color->green,
.blue = accent_color->blue,
.alpha = 0.0,
},
},
};
const GskColorStop hfadeout_mask_stops[] = {
{
0.0,
{
.red = 0.0,
.green = 0.0,
.blue = 0.0,
.alpha = 1.0,
},
},
{
0.75,
{
.red = 0.0,
.green = 0.0,
.blue = 0.0,
.alpha = 1.0,
},
},
{
1.0,
{
.red = 0.0,
.green = 0.0,
.blue = 0.0,
.alpha = 0.0,
},
},
};
gtk_snapshot_append_stroke (
snapshot,
transitioning != NULL
? transitioning
: self->path,
stroke, accent_color);
builder = gsk_path_builder_new ();
gsk_path_get_start_point (
transitioning != NULL
? transitioning
: self->path,
&point0);
gsk_path_point_get_position (
&point0,
transitioning != NULL
? transitioning
: self->path,
&start_position);
gsk_path_get_end_point (
transitioning != NULL
? transitioning
: self->path,
&point1);
gsk_path_point_get_position (
&point1,
transitioning != NULL
? transitioning
: self->path,
&end_position);
gsk_path_builder_move_to (builder, start_position.x, start_position.y);
gsk_path_builder_add_segment (
builder,
transitioning != NULL
? transitioning
: self->path,
&point0, &point1);
gsk_path_builder_move_to (builder, end_position.x, end_position.y);
gsk_path_builder_line_to (builder, end_position.x, widget_height - LABEL_MARGIN);
gsk_path_builder_line_to (builder, start_position.x, widget_height - LABEL_MARGIN);
gsk_path_builder_line_to (builder, start_position.x, start_position.y);
path = gsk_path_builder_to_path (builder);
gtk_snapshot_push_fill (
snapshot,
path,
GSK_FILL_RULE_WINDING);
gtk_snapshot_push_mask (snapshot, GSK_MASK_MODE_ALPHA);
gtk_snapshot_append_linear_gradient (
snapshot,
&GRAPHENE_RECT_INIT (
0.0,
0.0,
end_position.x,
widget_height - LABEL_MARGIN),
&GRAPHENE_POINT_INIT (0.0, 0.0),
&GRAPHENE_POINT_INIT (end_position.x, 0.0),
hfadeout_mask_stops,
G_N_ELEMENTS (hfadeout_mask_stops));
gtk_snapshot_pop (snapshot);
gtk_snapshot_append_linear_gradient (
snapshot,
&GRAPHENE_RECT_INIT (
0.0,
0.0,
end_position.x,
widget_height - LABEL_MARGIN),
&GRAPHENE_POINT_INIT (0.0, 0.0),
&GRAPHENE_POINT_INIT (0.0, widget_height - LABEL_MARGIN),
gradient_stops,
G_N_ELEMENTS (gradient_stops));
gtk_snapshot_pop (snapshot);
gtk_snapshot_pop (snapshot);
}
gtk_snapshot_restore (snapshot);
g_clear_pointer (&self->graph, gsk_render_node_unref);
self->graph = gtk_snapshot_to_node (snapshot);
}
static void
refresh_overlay (BzDataGraph *self)
{
double widget_width = 0.0;
double widget_height = 0.0;
GdkRGBA widget_color = { 0 };
AdwStyleManager *style_manager = NULL;
g_autoptr (GdkRGBA) accent_color = NULL;
guint n_items = 0;
guint hovered_idx = 0;
g_autoptr (BzDataPoint) point = NULL;
g_autoptr (GskStroke) crosshair_stroke = NULL;
double graph_height = 0.0;
double graph_width = 0.0;
double fraction = 0.0;
double point_x = 0.0;
double point_y = 0.0;
GskRoundedRect rounded_rect = { { { 0 } } };
GdkRGBA line_color = { 0 };
double rounded_axis_max = 0.0;
const char *prefix = NULL;
g_autofree char *line2_text = NULL;
g_autoptr (GtkSnapshot) snapshot = NULL;
if (self->path == NULL)
{
g_clear_pointer (&self->overlay, gsk_render_node_unref);
return;
}
widget_width = gtk_widget_get_width (GTK_WIDGET (self));
widget_height = gtk_widget_get_height (GTK_WIDGET (self));
if (self->motion_x < LABEL_MARGIN ||
self->motion_y < 0.0 ||
self->motion_x >= widget_width - LABEL_MARGIN_RIGHT ||
self->motion_y >= widget_height - LABEL_MARGIN)
{
g_clear_pointer (&self->overlay, gsk_render_node_unref);
return;
}
gtk_widget_get_color (GTK_WIDGET (self), &widget_color);
style_manager = adw_style_manager_get_default ();
accent_color = adw_style_manager_get_accent_color_rgba (style_manager);
n_items = g_list_model_get_n_items (self->model);
graph_width = widget_width - LABEL_MARGIN - LABEL_MARGIN_RIGHT;
fraction = (self->motion_x - LABEL_MARGIN) / graph_width;
hovered_idx = floor ((double) n_items * fraction);
if (hovered_idx >= n_items)
hovered_idx = n_items - 1;
point = g_list_model_get_item (self->model, hovered_idx);
if (self->rounded_axis_max > 0.0)
rounded_axis_max = self->rounded_axis_max;
else
{
double max_dependent = 0.0;
for (guint i = 0; i < n_items; i++)
{
g_autoptr (BzDataPoint) p = g_list_model_get_item (self->model, i);
double dep = bz_data_point_get_dependent (p);
if (i == 0 || dep > max_dependent)
max_dependent = dep;
}
rounded_axis_max = calculate_axis_tick_value (max_dependent, TRUE);
}
snapshot = gtk_snapshot_new ();
graph_height = widget_height - LABEL_MARGIN;
point_x = ((double) hovered_idx / (double) (n_items - 1)) * graph_width + LABEL_MARGIN;
point_y = (1.0 - bz_data_point_get_dependent (point) / rounded_axis_max) * graph_height;
line_color = widget_color;
line_color.alpha = 0.5;
crosshair_stroke = gsk_stroke_new (1.0);
#define APPEND_LINE(x0, y0, x1, y1, color) \
G_STMT_START \
{ \
g_autoptr (GskPathBuilder) builder = NULL; \
g_autoptr (GskPath) path = NULL; \
\
builder = gsk_path_builder_new (); \
gsk_path_builder_move_to (builder, (x0), (y0)); \
gsk_path_builder_line_to (builder, (x1), (y1)); \
\
path = gsk_path_builder_to_path (builder); \
gtk_snapshot_append_stroke (snapshot, path, crosshair_stroke, (color)); \
} \
G_STMT_END
APPEND_LINE (self->motion_x, 0.0, self->motion_x, widget_height - LABEL_MARGIN, &line_color);
#undef APPEND_LINE
gsk_rounded_rect_init_from_rect (
&rounded_rect,
&GRAPHENE_RECT_INIT (point_x - 4.0, point_y - 4.0, 8.0, 8.0),
4.0);
gtk_snapshot_push_rounded_clip (snapshot, &rounded_rect);
gtk_snapshot_append_color (snapshot, accent_color, &rounded_rect.bounds);
gtk_snapshot_pop (snapshot);
g_clear_pointer (&self->overlay, gsk_render_node_unref);
self->overlay = gtk_snapshot_to_node (snapshot);
gtk_label_set_label (GTK_LABEL (self->tooltip_label1), bz_data_point_get_label (point));
prefix = self->tooltip_prefix != NULL ? self->tooltip_prefix : "";
gtk_label_set_label (GTK_LABEL (self->tooltip_prefix_label), prefix);
line2_text = g_strdup_printf ("%'.0f", bz_data_point_get_dependent (point));
gtk_label_set_label (GTK_LABEL (self->tooltip_label2), line2_text);
}