/*
 *  $Id: graph-axis.c 28801 2025-11-05 11:52:53Z yeti-dn $
 *  Copyright (C) 2003-2025 David Necas (Yeti), Petr Klapetek.
 *  E-mail: yeti@gwyddion.net, klapetek@gwyddion.net.
 *
 *  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 2 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, write to the
 *  Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
 */

#include "config.h"
#include <string.h>
#include <stdlib.h>
#include <gtk/gtk.h>
#include <pango/pango.h>
#include <pango/pangocairo.h>

#include "libgwyddion/macros.h"
#include "libgwyddion/math.h"

#include "libgwyui/sci-text.h"
#include "libgwyui/graph-axis.h"
#include "libgwyui/graph.h"
#include "libgwyui/graph-axis-dialog.h"
#include "libgwyui/cairo-utils.h"
#include "libgwyui/widget-impl-utils.h"
#include "libgwyui/graph-internal.h"

enum {
    SGNL_RESCALED,
    NUM_SIGNALS
};

enum {
    PROP_0,
    PROP_LABEL,
    /* XXX: Where this stuff *really* belongs to? To the model? */
    PROP_AUTO,
    PROP_MAJOR_LENGTH,
    PROP_MAJOR_THICKNESS,
    PROP_MAJOR_MAXTICKS,
    PROP_MINOR_LENGTH,
    PROP_MINOR_THICKNESS,
    PROP_MINOR_DIVISION,
    NUM_PROPERTIES
};

/* FIXME GTK3 this is only used for an internal property which is always AUTO because there is not way to change
 * it. Remove? */
typedef enum {
    GWY_GRAPH_AXIS_SCALE_FORMAT_AUTO,
    GWY_GRAPH_AXIS_SCALE_FORMAT_EXP,
    GWY_GRAPH_AXIS_SCALE_FORMAT_INT
} GwyGraphAxisScaleFormat;

typedef struct {
    gdouble value;      /* Tick value, XXX but not really. For logscale is it already the logarithm. */
    gdouble scrpos;     /* Precomputed tick screen position. */
    gchar *text;
} GwyGraphAxisTick;

struct _GwyGraphAxisPrivate {
    GdkWindow *event_window;

    /* FIXME: should be floating point */
    gint major_length;
    gint major_thickness;
    gint major_maxticks;
    GwyGraphAxisScaleFormat major_printmode;

    gint minor_length;
    gint minor_thickness;
    gint minor_division;          /*minor division*/

    PangoFontDescription *major_font;
    PangoFontDescription *label_font;

    gboolean is_logarithmic;
    gboolean is_auto;           /*affects: tick numbers and label positions.*/
    GtkPositionType position;

    /* FIXME: we should not do this; just request enough space. */
    gboolean rerequest_size;

    gdouble reqmin;
    gdouble reqmax;
    gdouble max;                /* axis beginning value*/
    gdouble min;                /* axis end value*/

    GArray *mjticks;            /* array of GwyTicks */
    GArray *miticks;            /* array of GwyTicks, labels not set */

    GString *label_text;

    GwyUnit *unit;                /*axis unit (if any)*/
    gulong unit_changed_id;
    GString *magnification_string;
    gdouble magnification;

    GtkWidget *dialog;

    gboolean enable_label_edit;
};

static void            finalize            (GObject *object);
static void            dispose             (GObject *object);
static void            set_property        (GObject *object,
                                            guint prop_id,
                                            const GValue *value,
                                            GParamSpec *pspec);
static void            get_property        (GObject *object,
                                            guint prop_id,
                                            GValue *value,
                                            GParamSpec *pspec);
static void            notify              (GObject *object,
                                            GParamSpec *pspec);
static void            destroy             (GtkWidget *widget);
static void            realize             (GtkWidget *widget);
static void            unrealize           (GtkWidget *widget);
static void            map                 (GtkWidget *widget);
static void            unmap               (GtkWidget *widget);
static void            get_preferred_width (GtkWidget *widget,
                                            gint *minimum,
                                            gint *natural);
static void            get_preferred_height(GtkWidget *widget,
                                            gint *minimum,
                                            gint *natural);
static void            size_allocate       (GtkWidget *widget,
                                            GdkRectangle *allocation);
static gboolean        draw                (GtkWidget *widget,
                                            cairo_t *cr);
static gboolean        button_press        (GtkWidget *widget,
                                            GdkEventButton *event);
static void            screen_changed      (GtkWidget *widget,
                                            GdkScreen *previous_screen);
static gdouble         quantize_normal_tics(gdouble arg,
                                            gint guide);
static gboolean        normalscale         (GwyGraphAxis *a);
static gboolean        logscale            (GwyGraphAxis *a);
static gint            scale               (GwyGraphAxis *a);
static void            clear_ticks         (GwyGraphAxis *axis);
static gint            formatticks         (GwyGraphAxis *a);
static GwyValueFormat* calculate_format    (GwyGraphAxis *axis,
                                            GwyValueFormat *format);
static gint            precompute          (GwyGraphAxis *a,
                                            gint scrmin,
                                            gint scrmax);
static void            format_label        (GwyGraphAxis *axis,
                                            PangoLayout *layout);
static void            draw_tick_array     (GwyGraphAxis *axis,
                                            cairo_t *cr,
                                            GArray *array,
                                            gdouble length,
                                            gdouble line_width);
static void            draw_tick_labels    (GwyGraphAxis *axis,
                                            cairo_t *cr,
                                            GArray *array,
                                            gdouble tick_length);
static void            draw_label          (GwyGraphAxis *axis,
                                            cairo_t *cr);
static void            autoset             (GwyGraphAxis *axis,
                                            gint width,
                                            gint height);
static void            adjust              (GwyGraphAxis *axis);
static void            unit_changed        (GwyGraphAxis *axis,
                                            GwyUnit *unit);
#if 0
static void              label_edited        (GwySciText *sci_text,
                                              GwyGraphAxis *axis);
#endif
static void              free_axis_tick      (gpointer p);

static guint signals[NUM_SIGNALS] = { };
static GParamSpec *properties[NUM_PROPERTIES] = { NULL, };
static GtkWidgetClass *parent_class = NULL;

G_DEFINE_TYPE_WITH_CODE(GwyGraphAxis, gwy_graph_axis, GTK_TYPE_WIDGET,
                        G_ADD_PRIVATE(GwyGraphAxis))

static void
gwy_graph_axis_class_init(GwyGraphAxisClass *klass)
{
    GObjectClass *gobject_class = G_OBJECT_CLASS(klass);
    GtkWidgetClass *widget_class = GTK_WIDGET_CLASS(klass);
    GType type = G_TYPE_FROM_CLASS(klass);

    parent_class = gwy_graph_axis_parent_class;

    gobject_class->finalize = finalize;
    gobject_class->dispose = dispose;
    gobject_class->set_property = set_property;
    gobject_class->get_property = get_property;
    gobject_class->notify = notify;

    widget_class->destroy = destroy;
    widget_class->realize = realize;
    widget_class->unrealize = unrealize;
    widget_class->map = map;
    widget_class->unmap = unmap;
    widget_class->draw = draw;
    widget_class->get_preferred_width = get_preferred_width;
    widget_class->get_preferred_height = get_preferred_height;
    widget_class->size_allocate = size_allocate;
    widget_class->button_press_event = button_press;
    widget_class->screen_changed = screen_changed;

    signals[SGNL_RESCALED] = g_signal_new("rescaled", type,
                                          G_SIGNAL_RUN_FIRST,
                                          G_STRUCT_OFFSET(GwyGraphAxisClass, rescaled),
                                          NULL, NULL,
                                          g_cclosure_marshal_VOID__VOID,
                                          G_TYPE_NONE, 0);
    g_signal_set_va_marshaller(signals[SGNL_RESCALED], type, g_cclosure_marshal_VOID__VOIDv);

    properties[PROP_LABEL] = g_param_spec_string("label", NULL,
                                                 "Axis label (without units)",
                                                 "",
                                                 GWY_GPARAM_RWE);
    properties[PROP_AUTO] = g_param_spec_boolean("auto", NULL,
                                                 "Autoscale ticks with changing content",
                                                 TRUE,
                                                 GWY_GPARAM_RWE);
    properties[PROP_MAJOR_LENGTH] = g_param_spec_int("major-length", NULL,
                                                     "Major ticks length",
                                                     0, 20, 5,
                                                     GWY_GPARAM_RWE);
    properties[PROP_MAJOR_THICKNESS] = g_param_spec_int("major-thickness", NULL,
                                                        "Major ticks thickness",
                                                        0, 20, 5,
                                                        GWY_GPARAM_RWE);
    properties[PROP_MAJOR_MAXTICKS] = g_param_spec_int("major-maxticks", NULL,
                                                       "Major ticks maximum number",
                                                       0, 50, 5,
                                                       GWY_GPARAM_RWE);
    properties[PROP_MINOR_LENGTH] = g_param_spec_int("minor-length", NULL,
                                                     "Minor ticks length",
                                                     0, 20, 5,
                                                     GWY_GPARAM_RWE);
    properties[PROP_MINOR_THICKNESS] = g_param_spec_int("minor-thickness", NULL,
                                                        "Minor ticks thickness",
                                                        0, 20, 5,
                                                        GWY_GPARAM_RWE);
    properties[PROP_MINOR_DIVISION] = g_param_spec_int("minor-division", NULL,
                                                       "Minor ticks division",
                                                       0, 20, 5,
                                                       GWY_GPARAM_RWE);

    g_object_class_install_properties(gobject_class, NUM_PROPERTIES, properties);
}

static void
gwy_graph_axis_init(GwyGraphAxis *axis)
{
    GwyGraphAxisPrivate *priv;

    priv = axis->priv = gwy_graph_axis_get_instance_private(axis);

    priv->position = GTK_POS_BOTTOM;

    priv->is_auto = TRUE;
    priv->major_printmode = GWY_GRAPH_AXIS_SCALE_FORMAT_AUTO;

    priv->major_length = 10;
    priv->major_thickness = 1;
    priv->major_maxticks = 20;

    priv->minor_length = 5;
    priv->minor_thickness = 1;
    priv->minor_division = 10;

    priv->reqmax = 1.0;
    priv->reqmin = 0.0;

    priv->enable_label_edit = TRUE;

    priv->unit = gwy_unit_new(NULL);
    priv->unit_changed_id = g_signal_connect_swapped(priv->unit, "value-changed", G_CALLBACK(unit_changed), axis);
    priv->magnification = 1;

    priv->mjticks = g_array_new(FALSE, FALSE, sizeof(GwyGraphAxisTick));
    g_array_set_clear_func(priv->mjticks, free_axis_tick);
    priv->miticks = g_array_new(FALSE, FALSE, sizeof(GwyGraphAxisTick));
    // XXX: Not necessary here.
    //g_array_set_clear_func(priv->miticks, free_axis_tick);
    priv->label_text = g_string_new(NULL);

    gtk_widget_set_has_window(GTK_WIDGET(axis), FALSE);

    /* Set up fonts. */
    screen_changed(GTK_WIDGET(axis), NULL);
}

/**
 * gwy_graph_axis_new:
 * @position: Axis position (implying orientation).
 *
 * Creates a new axis.
 *
 * Returns: New axis as a #GtkWidget.
 **/
GtkWidget*
gwy_graph_axis_new(GtkPositionType position)
{
    GwyGraphAxis *axis = g_object_new(GWY_TYPE_GRAPH_AXIS, NULL);
    axis->priv->position = position;
    return GTK_WIDGET(axis);
}

static void
finalize(GObject *object)
{
    GwyGraphAxis *axis = GWY_GRAPH_AXIS(object);
    GwyGraphAxisPrivate *priv = axis->priv;

    GWY_FREE_STRING(priv->label_text);
    GWY_FREE_STRING(priv->magnification_string);
    clear_ticks(axis);
    GWY_FREE_ARRAY(priv->mjticks);
    GWY_FREE_ARRAY(priv->miticks);

    g_clear_object(&priv->unit);

    if (priv->major_font)
        pango_font_description_free(priv->major_font);
    if (priv->label_font)
        pango_font_description_free(priv->label_font);

    G_OBJECT_CLASS(parent_class)->finalize(object);
}

static void
dispose(GObject *object)
{
    GwyGraphAxis *axis = GWY_GRAPH_AXIS(object);
    GwyGraphAxisPrivate *priv = axis->priv;

    g_clear_signal_handler(&priv->unit_changed_id, priv->unit);
}

static void
free_axis_tick(gpointer p)
{
    GwyGraphAxisTick *tick = (GwyGraphAxisTick*)p;
    g_free(tick->text);
}

static void
destroy(GtkWidget *widget)
{
    GwyGraphAxis *axis = GWY_GRAPH_AXIS(widget);
    GwyGraphAxisPrivate *priv = axis->priv;

    if (priv->dialog) {
        gtk_widget_destroy(priv->dialog);
        priv->dialog = NULL;
    }

    GTK_WIDGET_CLASS(parent_class)->destroy(widget);
}

static void
set_property(GObject *object,
             guint prop_id,
             const GValue *value,
             GParamSpec *pspec)
{
    GwyGraphAxis *axis = GWY_GRAPH_AXIS(object);
    GwyGraphAxisPrivate *priv = axis->priv;
    gboolean b, changed = FALSE;
    gint i;

    switch (prop_id) {
        case PROP_LABEL:
        gwy_graph_axis_set_label(axis, g_value_get_string(value));
        break;

        case PROP_AUTO:
        if ((changed = (priv->is_auto != (b = g_value_get_boolean(value)))))
            priv->is_auto = b;
        break;

        case PROP_MAJOR_LENGTH:
        if ((changed = (priv->major_length != (i = g_value_get_int(value)))))
            priv->major_length = i;
        break;

        case PROP_MAJOR_THICKNESS:
        if ((changed = (priv->major_thickness != (i = g_value_get_int(value)))))
            priv->major_thickness = i;
        break;

        case PROP_MAJOR_MAXTICKS:
        if ((changed = (priv->major_maxticks != (i = g_value_get_int(value)))))
            priv->major_maxticks = i;
        break;

        case PROP_MINOR_LENGTH:
        if ((changed = (priv->minor_length != (i = g_value_get_int(value)))))
            priv->minor_length = i;
        break;

        case PROP_MINOR_THICKNESS:
        if ((changed = (priv->minor_thickness != (i = g_value_get_int(value)))))
            priv->minor_thickness = i;
        break;

        case PROP_MINOR_DIVISION:
        if ((changed = (priv->minor_division != (i = g_value_get_int(value)))))
            priv->minor_division = i;
        break;

        default:
        G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
        break;
    }

    if (changed)
        g_object_notify_by_pspec(object, properties[prop_id]);
}

static void
get_property(GObject *object,
             guint prop_id,
             GValue *value,
             GParamSpec *pspec)
{
    GwyGraphAxisPrivate *priv = GWY_GRAPH_AXIS(object)->priv;

    switch (prop_id) {
        case PROP_LABEL:
        g_value_set_string(value, priv->label_text->str);
        break;

        case PROP_AUTO:
        g_value_set_boolean(value, priv->is_auto);
        break;

        case PROP_MAJOR_LENGTH:
        g_value_set_int(value, priv->major_length);
        break;

        case PROP_MAJOR_THICKNESS:
        g_value_set_int(value, priv->major_thickness);
        break;

        case PROP_MAJOR_MAXTICKS:
        g_value_set_int(value, priv->major_maxticks);
        break;

        case PROP_MINOR_LENGTH:
        g_value_set_int(value, priv->minor_length);
        break;

        case PROP_MINOR_THICKNESS:
        g_value_set_int(value, priv->minor_thickness);
        break;

        case PROP_MINOR_DIVISION:
        g_value_set_int(value, priv->minor_division);
        break;

        default:
        G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
        break;
    }
}

static void
realize(GtkWidget *widget)
{
    GwyGraphAxis *axis = GWY_GRAPH_AXIS(widget);
    GwyGraphAxisPrivate *priv = axis->priv;

    /* FIXME GTK3 widgets generally do not call parent's realize. Not sure why because it does a couple of things for
     * us like setting the widget realized. */
    parent_class->realize(widget);
    priv->event_window = gwy_create_widget_input_window(widget,
                                                        GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK
                                                        | GDK_POINTER_MOTION_MASK);
    adjust(axis);
}

static void
unrealize(GtkWidget *widget)
{
    GwyGraphAxisPrivate *priv = GWY_GRAPH_AXIS(widget)->priv;
    gwy_destroy_widget_input_window(widget, &priv->event_window);
    GTK_WIDGET_CLASS(parent_class)->unrealize(widget);
}

static void
map(GtkWidget *widget)
{
    GwyGraphAxisPrivate *priv = GWY_GRAPH_AXIS(widget)->priv;
    parent_class->map(widget);
    gdk_window_show(priv->event_window);
}

static void
unmap(GtkWidget *widget)
{
    GwyGraphAxisPrivate *priv = GWY_GRAPH_AXIS(widget)->priv;
    gdk_window_hide(priv->event_window);
    GTK_WIDGET_CLASS(parent_class)->unmap(widget);
}

/* FIXME: Remember it or something. We compute both sizes simultaneously but we do not have to do it always twice. */
static void
request_size(GwyGraphAxis *axis, GtkRequisition *requisition)
{
    GwyGraphAxisPrivate *priv = axis->priv;
    PangoRectangle rect_label, rect;
    PangoLayout *layout;
    const GwyGraphAxisTick *pmjt;
    guint i;

    requisition->width = requisition->height = 0;

    layout = gtk_widget_create_pango_layout(GTK_WIDGET(axis), "");
    format_label(axis, layout);
    pango_layout_get_pixel_extents(layout, NULL, &rect_label);

    gint sep = 3;

    if (priv->position == GTK_POS_LEFT || priv->position == GTK_POS_RIGHT) {
        requisition->height += rect_label.width + 2*sep;
        requisition->width += rect_label.height;
        requisition->width += priv->major_length;
        requisition->width += 2*sep;

        /* FIXME: This does not work, at this point we do not know the real tick labels yet */
        rect_label.width = 0;
        pango_layout_set_font_description(layout, priv->major_font);
        for (i = 0; i < priv->mjticks->len; i++) {
            pmjt = &g_array_index(priv->mjticks, GwyGraphAxisTick, i);
            pango_layout_set_markup(layout, pmjt->text, -1);
            pango_layout_get_pixel_extents(layout, NULL, &rect);
            rect_label.width = MAX(rect_label.width, rect.width);
        }
        if (!priv->mjticks->len) {
            priv->rerequest_size++;
            if (G_UNLIKELY(priv->rerequest_size > 3)) {
                g_warning("Axis size rerequest repeated 3 times, giving up");
                priv->rerequest_size = 0;
            }
        }
        else
            priv->rerequest_size = 0;
        gwy_debug("%p must rerequest: %d", axis, priv->rerequest_size);
        requisition->width += rect_label.width;
    }
    else if (priv->position == GTK_POS_BOTTOM || priv->position == GTK_POS_TOP) {
        requisition->width += rect_label.width + 2*sep;
        requisition->height += rect_label.height;
        requisition->height += priv->major_length;
        requisition->height += 2*sep;

        pango_layout_set_text(layout, "0.9", -1);
        pango_layout_get_pixel_extents(layout, NULL, &rect_label);
        requisition->height += rect_label.height;
    }

    g_object_unref(layout);
}

static void
get_preferred_width(GtkWidget *widget,
                    gint *minimum, gint *natural)
{
    GtkRequisition requisition;

    request_size(GWY_GRAPH_AXIS(widget), &requisition);
    *minimum = *natural = requisition.width;
}

static void
get_preferred_height(GtkWidget *widget,
                     gint *minimum, gint *natural)
{
    GtkRequisition requisition;

    request_size(GWY_GRAPH_AXIS(widget), &requisition);
    *minimum = *natural = requisition.height;
}

static void
size_allocate(GtkWidget *widget,
              GdkRectangle *allocation)
{
    GwyGraphAxis *axis = GWY_GRAPH_AXIS(widget);
    GwyGraphAxisPrivate *priv = axis->priv;

    parent_class->size_allocate(widget, allocation);

    if (priv->event_window)
        gdk_window_move_resize(priv->event_window, allocation->x, allocation->y, allocation->width, allocation->height);

    adjust(axis);
}

static void
adjust(GwyGraphAxis *axis)
{
    GtkWidget *widget = GTK_WIDGET(axis);
    GwyGraphAxisPrivate *priv = axis->priv;

    gwy_debug("%p(%d)", axis, priv->position);

    gint width = gtk_widget_get_allocated_width(widget);
    gint height = gtk_widget_get_allocated_height(widget);
    if (!gtk_widget_is_visible(widget)) {
        gwy_debug("using area's size");
        /* FIXME: This is mostly a hack. */
        GtkWidget *graph = gtk_widget_get_ancestor(widget, GWY_TYPE_GRAPH);
        if (graph) {
            GtkWidget *area = gwy_graph_get_area(GWY_GRAPH(graph));
            if (priv->position == GTK_POS_TOP || priv->position == GTK_POS_BOTTOM)
                width = gtk_widget_get_allocated_width(area);
            else
                height = gtk_widget_get_allocated_height(area);
        }
    }
    gwy_debug("width %d, height %d", width, height);
    gwy_debug("requested range %g..%g", priv->reqmin, priv->reqmax);

    if (priv->is_auto)
        autoset(axis, width, height);
    gint iterations = 0;
    if (priv->is_logarithmic)
        scale(axis);
    else {
        gint scaleres;
        do {
            scaleres = scale(axis);
            if (scaleres > 0)
                priv->major_maxticks = priv->major_maxticks/2;
            if (scaleres < 0)
                priv->major_maxticks = 2*priv->major_maxticks;
            priv->major_maxticks = MAX(priv->major_maxticks, 1);
            //gwy_debug("scale: %d, iterations: %d, maxticks %d", scaleres, iterations, priv->major_maxticks);
            iterations++;
        } while (scaleres != 0 && iterations < 10);
    }
    if (priv->position == GTK_POS_TOP || priv->position == GTK_POS_BOTTOM)
        precompute(axis, 0, width);
    else
        precompute(axis, 0, height);

    g_signal_emit(axis, signals[SGNL_RESCALED], 0);

    /* FIXME: When tick format changes we probably need to request another resize. */
    if (priv->rerequest_size) {
        gwy_debug("%p issuing rerequest", axis);
        gtk_widget_queue_resize(GTK_WIDGET(axis));
    }
    if (gtk_widget_is_drawable(GTK_WIDGET(axis)))
        gtk_widget_queue_draw(GTK_WIDGET(axis));
}

void
_gwy_graph_axis_adjust(GwyGraphAxis *axis)
{
    g_return_if_fail(GWY_IS_GRAPH_AXIS(axis));
    adjust(axis);
}

static void
autoset(GwyGraphAxis *axis, gint width, gint height)
{
    GwyGraphAxisPrivate *priv = axis->priv;

    if (priv->position == GTK_POS_TOP || priv->position == GTK_POS_BOTTOM) {
        priv->major_maxticks = width/50; /*empirical equation*/
        if (width < 300)
            priv->minor_division = 5;
        else
            priv->minor_division = 10;
    }
    if (priv->position == GTK_POS_RIGHT || priv->position == GTK_POS_LEFT) {
        priv->major_maxticks = height/40; /*empirical equation*/
        if (height < 150)
            priv->minor_division = 5;
        else
            priv->minor_division = 10;
    }
}

/* FIXME: ugly. */
static void
notify(GObject *object, GParamSpec *pspec)
{
    void (*parent_notify)(GObject*, GParamSpec*) = G_OBJECT_CLASS(parent_class)->notify;

    if (parent_notify)
        parent_notify(object, pspec);

    if (g_type_is_a(pspec->owner_type, GWY_TYPE_GRAPH_AXIS))
        adjust(GWY_GRAPH_AXIS(object));
}

/**
 * gwy_graph_axis_set_logarithmic:
 * @axis: graph axis
 * @is_logarithmic: logarithmic mode
 *
 * Sets logarithmic mode.
 **/
void
gwy_graph_axis_set_logarithmic(GwyGraphAxis *axis,
                               gboolean is_logarithmic)
{
    g_return_if_fail(GWY_IS_GRAPH_AXIS(axis));

    GwyGraphAxisPrivate *priv = axis->priv;
    if (!priv->is_logarithmic == !is_logarithmic)
        return;

    priv->is_logarithmic = !!is_logarithmic;
    adjust(axis);
}

static gboolean
draw(GtkWidget *widget, cairo_t *cr)
{
    GwyGraphAxis *axis = GWY_GRAPH_AXIS(widget);
    GwyGraphAxisPrivate *priv = axis->priv;

    cairo_save(cr);
    cairo_set_source_rgb(cr, 1.0, 1.0, 1.0);
    cairo_paint(cr);
    cairo_restore(cr);

    draw_tick_array(axis, cr, priv->mjticks, priv->major_length, priv->major_thickness);
    draw_tick_array(axis, cr, priv->miticks, priv->minor_length, priv->minor_thickness);
    draw_tick_labels(axis, cr, priv->mjticks, priv->major_length);
    draw_label(axis, cr);

    return FALSE;
}

static void
draw_tick_array(GwyGraphAxis *axis, cairo_t *cr,
                GArray *array, gdouble length, gdouble line_width)
{
    GdkRectangle alloc;
    gtk_widget_get_allocation(GTK_WIDGET(axis), &alloc);

    cairo_save(cr);
    cairo_set_source_rgb(cr, 0.0, 0.0, 0.0);
    cairo_set_line_width(cr, line_width);
    cairo_set_line_cap(cr, CAIRO_LINE_CAP_BUTT);

    GtkPositionType edge = axis->priv->position;
    GwyGraphAxisTick *ticks = &g_array_index(array, GwyGraphAxisTick, 0);
    gint width = alloc.width, height = alloc.height;
    if (edge == GTK_POS_BOTTOM) {
        for (guint i = 0; i < array->len; i++)
            gwy_cairo_line(cr, ticks[i].scrpos, 0, ticks[i].scrpos, length);
    }
    else if (edge == GTK_POS_TOP) {
        for (guint i = 0; i < array->len; i++)
            gwy_cairo_line(cr, ticks[i].scrpos, height, ticks[i].scrpos, height - length);
    }
    else if (edge == GTK_POS_LEFT) {
        for (guint i = 0; i < array->len; i++)
            gwy_cairo_line(cr, width - length, ticks[i].scrpos, width, ticks[i].scrpos);
    }
    else if (edge == GTK_POS_RIGHT) {
        for (guint i = 0; i < array->len; i++)
            gwy_cairo_line(cr, 0, ticks[i].scrpos, length, ticks[i].scrpos);
    }

    cairo_stroke(cr);
    cairo_restore(cr);
}

static void
draw_tick_labels(GwyGraphAxis *axis, cairo_t *cr,
                 GArray *array, gdouble tick_length)
{
    GdkRectangle alloc;
    gtk_widget_get_allocation(GTK_WIDGET(axis), &alloc);

    cairo_save(cr);
    cairo_set_source_rgb(cr, 0.0, 0.0, 0.0);

    GwyGraphAxisPrivate *priv = axis->priv;
    GtkPositionType edge = priv->position;

    PangoLayout *layout = gtk_widget_create_pango_layout(GTK_WIDGET(axis), "");
    pango_layout_set_font_description(layout, priv->major_font);

    GwyGraphAxisTick *ticks = &g_array_index(array, GwyGraphAxisTick, 0);
    gint width = alloc.width, height = alloc.height, sep = 3;
    for (guint i = 0; i < array->len; i++) {
        PangoRectangle rect;
        g_return_if_fail(ticks[i].text);
        pango_layout_set_markup(layout, ticks[i].text, -1);
        pango_layout_get_pixel_extents(layout, NULL, &rect);

        gdouble xpos, ypos;
        if (edge == GTK_POS_BOTTOM) {
            xpos = ticks[i].scrpos - rect.width/2;
            ypos = tick_length + sep;
        }
        else if (edge == GTK_POS_TOP) {
            xpos = ticks[i].scrpos - rect.width/2;
            ypos = height-1 - tick_length - rect.height;
        }
        else if (edge == GTK_POS_RIGHT) {
            xpos = tick_length + sep;
            ypos = height-1 - ticks[i].scrpos - rect.height/2;
        }
        else if (edge == GTK_POS_LEFT) {
            xpos = width-1 - tick_length - sep - rect.width;
            ypos = height-1 - ticks[i].scrpos - rect.height/2;
        }
        else {
            g_assert_not_reached();
        }

        xpos = CLAMP(xpos, 0, width-1 - rect.width);
        ypos = CLAMP(ypos, 0, height-1 - rect.height);

        cairo_move_to(cr, xpos, ypos);
        /* XXX: Cannot use gtk_render_layout() because that renders the layout as it would a label in a widget.
         * Which may be completely inappropriate for the plot because it has its own style. */
        pango_cairo_show_layout(cr, layout);
    }

    g_object_unref(layout);

    cairo_restore(cr);
}

static void
format_label(GwyGraphAxis *axis,
             PangoLayout *layout)
{
    GwyGraphAxisPrivate *priv = axis->priv;
    GString *plotlabel = g_string_new(priv->label_text->str);
    gchar *units = gwy_unit_get_string(priv->unit, GWY_UNIT_FORMAT_MARKUP);

    if ((priv->magnification_string && priv->magnification_string->len > 0) || *units) {
        g_string_append(plotlabel, " [");
        if (priv->magnification_string && priv->magnification_string->len)
            g_string_append(plotlabel, priv->magnification_string->str);
        else
            g_string_append(plotlabel, units);
        g_string_append(plotlabel, "]");
    }
    g_free(units);

    pango_layout_set_font_description(layout, priv->major_font);
    pango_layout_set_markup(layout, plotlabel->str, plotlabel->len);
    g_string_free(plotlabel, TRUE);
}

static void
draw_label(GwyGraphAxis *axis, cairo_t *cr)
{
    GdkRectangle alloc;
    gtk_widget_get_allocation(GTK_WIDGET(axis), &alloc);

    cairo_save(cr);
    cairo_set_source_rgb(cr, 0.0, 0.0, 0.0);

    GwyGraphAxisPrivate *priv = axis->priv;
    GtkPositionType edge = priv->position;

    PangoContext *context = gtk_widget_create_pango_context(GTK_WIDGET(axis));
    PangoLayout *layout = pango_layout_new(context);
    PangoRectangle rect;
    format_label(axis, layout);
    pango_layout_get_pixel_extents(layout, NULL, &rect);

    /* XXX: pango_cairo_show_layout() ignores Pango rotation. gtk_render_layout() uses styling as for a label,
     * which might be weird according to the theme. Must use Cairo rotation. */
    gint width = alloc.width, height = alloc.height;
    if (edge == GTK_POS_BOTTOM)
        cairo_move_to(cr, width/2 - rect.width/2, height - rect.height);
    else if (edge == GTK_POS_TOP)
        cairo_move_to(cr, width/2 - rect.width/2, 0);
    else if (edge == GTK_POS_RIGHT) {
        cairo_rotate(cr, -0.5*G_PI);
        cairo_move_to(cr, -height/2 - rect.width/2, width - rect.height);
    }
    else if (edge == GTK_POS_LEFT) {
        cairo_rotate(cr, -0.5*G_PI);
        cairo_move_to(cr, -height/2 - rect.width/2, 0);
    }
    else {
        g_assert_not_reached();
    }

    pango_cairo_show_layout(cr, layout);

    g_object_unref(layout);
    g_object_unref(context);

    cairo_restore(cr);
}

static gboolean
button_press(GtkWidget *widget, GdkEventButton *event)
{
    GwyGraphAxis *axis = GWY_GRAPH_AXIS(widget);
    GwyGraphAxisPrivate *priv = axis->priv;

    if (event->button != 1)
        return FALSE;

    if (priv->enable_label_edit) {
        if (!priv->dialog)
            priv->dialog = _gwy_graph_axis_dialog_new(axis);
        gtk_widget_show_all(priv->dialog);
        gtk_window_present(GTK_WINDOW(priv->dialog));
        return TRUE;
    }

    return FALSE;
}

static void
screen_changed(GtkWidget *widget,
               G_GNUC_UNUSED GdkScreen *previous_screen)
{
    PangoContext *context = gtk_widget_get_pango_context(widget);
    PangoFontDescription *description = pango_context_get_font_description(context);
    GwyGraphAxisPrivate *priv = GWY_GRAPH_AXIS(widget)->priv;

    if (priv->major_font)
        pango_font_description_free(priv->major_font);
    if (priv->label_font)
        pango_font_description_free(priv->label_font);

    /* Make major font a bit smaller */
    priv->major_font = pango_font_description_copy(description);
    gint size = pango_font_description_get_size(priv->major_font);
    size = MAX(1, size*10/11);
    pango_font_description_set_size(priv->major_font, size);

    /* Keep label font to default. */
    priv->label_font = pango_font_description_copy(description);
}

#if 0
static void
label_edited(GwySciText *sci_text, GwyGraphAxis *axis)
{
    g_assert(GWY_IS_GRAPH_AXIS(axis));
    gchar *text;

    text = gwy_sci_text_get_text(sci_text);
    if (gwy_strequal(text, priv->label_text->str)) {
        g_free(text);
        return;
    }
    g_string_assign(priv->label_text, text);
    g_free(text);
    g_object_notify_by_pspec(G_OBJECT(axis), properties[PROP_LABEL]);
}
#endif

static gdouble
quantize_normal_tics(gdouble arg, gint guide)
{
    gdouble power = gwy_powi(10.0, (gint)floor(log10(arg)));
    gdouble xnorm = arg/power;        /* approx number of decades */
    gdouble posns = guide/xnorm; /* approx number of tic posns per decade */
    gdouble tics;

    if (posns > 40)
        tics = 0.05;            /* eg 0, .05, .10, ... */
    else if (posns > 20)
        tics = 0.1;             /* eg 0, .1, .2, ... */
    else if (posns > 10)
        tics = 0.2;             /* eg 0,0.2,0.4,... */
    else if (posns > 4)
        tics = 0.5;             /* 0,0.5,1, */
    else if (posns > 1)
        tics = 1;               /* 0,1,2,.... */
    else if (posns > 0.5)
        tics = 2;               /* 0, 2, 4, 6 */
    else if (posns > 0.1)
        tics = 5;               /* 0, 5, 10, 15*/
    else
        tics = ceil(xnorm);

    return tics * power;
}

static gboolean
normalscale(GwyGraphAxis *axis)
{
    GwyGraphAxisPrivate *priv = axis->priv;

    /* Do something reasonable even with silly requests, they can easily occur when graph model properties are updated
     * sequentially */
    gdouble reqmin = MIN(priv->reqmin, priv->reqmax);
    gdouble reqmax = MAX(priv->reqmax, priv->reqmin);
    if (reqmax == reqmin) {
        if (reqmax == 0.0) {
            reqmin = 0.0;
            reqmax = 1.0;
        }
        else {
            gdouble range = fabs(reqmax);
            reqmax += range/2.0;
            reqmin -= range/2.0;
        }
    }
    gwy_debug("%p: reqmin: %g, reqmax: %g", axis, reqmin, reqmax);

    gdouble range = fabs(reqmax - reqmin); /* total range of the axis */

    if (range > G_MAXDOUBLE/100.0 || range < -G_MAXDOUBLE/100.0) {
        g_warning("Axis with extreme range!");
        return TRUE;
    }

    /* Step. */
    gdouble tickstep = quantize_normal_tics(range, priv->major_maxticks);
    gdouble majorbase = ceil(reqmin/tickstep)*tickstep; /*starting value*/
    gdouble minortickstep = tickstep/priv->minor_division;
    gdouble minorbase = ceil(reqmin/minortickstep)*minortickstep;
    gwy_debug("majorbase: %g, tickstep: %g, minorbase: %g: minortickstep %g",
              majorbase, tickstep, minorbase, minortickstep);
    if (majorbase > reqmin) {
        majorbase -= tickstep;
        minorbase = majorbase;
        priv->min = majorbase;
    }
    else
        priv->min = reqmin;

    /* Major ticks. */
    gint i = 0;
    do {
        GwyGraphAxisTick tick = {
            .value = majorbase,
            .text = NULL,
        };
        g_array_append_val(priv->mjticks, tick);
        majorbase += tickstep;
        i++;
    } while (majorbase - tickstep < reqmax && i < 2*priv->major_maxticks);
    priv->max = majorbase - tickstep;


    i = 0;
    /* Minor ticks. */
    do {
        GwyGraphAxisTick tick = {
            .value = minorbase,
            .text = NULL,
        };
        g_array_append_val(priv->miticks, tick);
        minorbase += minortickstep;
        i++;
    } while (minorbase <= priv->max && i < 20*priv->major_maxticks);

    gwy_debug("min: %g, max: %g", priv->min, priv->max);

    return TRUE;
}

static gboolean
logscale(GwyGraphAxis *axis)
{
    GwyGraphAxisPrivate *priv = axis->priv;

    gdouble max = MAX(priv->reqmax, priv->reqmin);
    gdouble min = MIN(priv->reqmin, priv->reqmax);

    if (min < 0.0 || (min == max && max == 0.0))
        return FALSE;

    /* No negative values are allowed, do anything just don't crash... */
    gdouble logmin, logmax = log10(max);
    if (min > 0.0)
        logmin = log10(min);
    else
        logmin = logmax - 1.0;

    /* Ticks will be linearly distributed again */
    /* Major ticks - will be equally ditributed in the log domain 1,10,100 */
    gint i, n = MAX(priv->major_maxticks - 1, 1);
    gdouble tickstep = ceil((ceil(logmax) - floor(logmin))/n);
    gdouble base;
    for (i = 0; i < 3; i++) {
        base = (ceil(logmin/tickstep) - 1)*tickstep; /* starting value */
        if (base <= logmin && base + n*tickstep >= logmax)
            break;
        tickstep += 1.0;
    }
    logmin = base;
    i = 0;
    do {
        GwyGraphAxisTick tick = {
            .value = base,
            .text = NULL,
        };
        g_array_append_val(priv->mjticks, tick);
        base += tickstep;
        i++;
    } while (i < 2 || ((base - tickstep) < logmax && i < priv->major_maxticks));
    logmax = base - tickstep;

    min = gwy_powi(10.0, logmin);
    max = gwy_powi(10.0, logmax);

    /* Minor ticks - will be equally distributed in the normal domain 1,2,3... if the major tick step is only one
     * order, otherwise distribute them in the log domain too */
    if (tickstep == 1) {
        tickstep = min;
        base = tickstep;
        i = 0;
        do {
            /* Here, tickstep must be adapted do scale */
            tickstep = gwy_powi(10.0, (gint)floor(log10(base*1.001)));
            GwyGraphAxisTick tick = {
                .value = log10(base),
                .text = NULL,
            };
            g_array_append_val(priv->miticks, tick);
            base += tickstep;
            i++;
        } while (base <= max && i < priv->major_maxticks*20);
    }
    else {
        i = 0;
        tickstep = 1;
        base = logmin;
        do {
            GwyGraphAxisTick tick = {
                .value = log10(base),
                .text = NULL,
            };
            g_array_append_val(priv->miticks, tick);
            base += tickstep;
            i++;
        } while (base - tickstep < logmax && i < priv->major_maxticks*20);
    }

    priv->max = max;
    priv->min = min;

    return TRUE;
}

static void
clear_ticks(GwyGraphAxis *axis)
{
    GwyGraphAxisPrivate *priv = axis->priv;
    g_array_set_size(priv->mjticks, 0);
    g_array_set_size(priv->miticks, 0);
}

/* returns
 * 0 if everything went OK,
 * < 0 if there are not enough major ticks,
 * > 0 if there area too many ticks */
static gint
scale(GwyGraphAxis *axis)
{
    GwyGraphAxisPrivate *priv = axis->priv;

    /* Never use logarithmic mode for negative numbers. */
    if (priv->reqmin < 0 && priv->is_logarithmic)
        return 1; /*this is an error*/

    clear_ticks(axis);

    /*find tick positions*/
    if (priv->is_logarithmic)
        logscale(axis);
    else
        normalscale(axis);

    /* Displaying NaN or similar rubbish, just return success... */
    if (!priv->mjticks->len)
        return 0;

    /*label ticks*/
    return formatticks(axis);
}

/* precompute screen coordinates of ticks (must be done after each geometry change) */
static gint
precompute(GwyGraphAxis *axis, gint scrmin, gint scrmax)
{
    GwyGraphAxisPrivate *priv = axis->priv;
    guint i;
    gdouble dist, range, dr;
    GwyGraphAxisTick *pmjt;
    GwyGraphAxisTick *pmit;

    dist = scrmax - scrmin - 1;
    if (priv->is_logarithmic)
        range = log10(priv->max/priv->min);
    else
        range = priv->max - priv->min;

    /* TODO: Use the common screen-real conversion function. But with some hinting. */
    dr = dist/range;
    for (i = 0; i < priv->mjticks->len; i++) {
        pmjt = &g_array_index(priv->mjticks, GwyGraphAxisTick, i);
        if (priv->is_logarithmic)
            pmjt->scrpos = GWY_ROUND(scrmin + (pmjt->value - log10(priv->min))*dr) + 0.5;
        else
            pmjt->scrpos = GWY_ROUND(scrmin + (pmjt->value - priv->min)*dr) + 0.5;
    }

    for (i = 0; i < priv->miticks->len; i++) {
        pmit = &g_array_index(priv->miticks, GwyGraphAxisTick, i);
        if (priv->is_logarithmic)
            pmit->scrpos = GWY_ROUND(scrmin + (pmit->value - log10(priv->min))*dr) + 0.5;
        else
            pmit->scrpos = GWY_ROUND(scrmin + (pmit->value - priv->min)*dr) + 0.5;
    }

    return 0;
}

static gint
formatticks(GwyGraphAxis *axis)
{
    GwyGraphAxisPrivate *priv = axis->priv;
    gdouble value;
    PangoLayout *layout;
    PangoContext *context;
    PangoRectangle rect;
    gint totalwidth = 0, totalheight = 0;
    GwyValueFormat *format = NULL;
    GwyGraphAxisTick *pmjt;
    gboolean human_fmt = TRUE;
    guint i;

    /* Determine range */
    if (priv->mjticks->len == 0) {
        g_warning("No ticks found");
        return 1;
    }

    /* move exponents to axis label */
    if (priv->major_printmode == GWY_GRAPH_AXIS_SCALE_FORMAT_AUTO) {
        format = calculate_format(axis, format);
        if (priv->magnification_string)
            g_string_assign(priv->magnification_string, format->units);
        else
            priv->magnification_string = g_string_new(format->units);
        priv->magnification = format->magnitude;
    }
    else {
        if (priv->magnification_string)
            g_string_free(priv->magnification_string, TRUE);
        priv->magnification_string = NULL;
        priv->magnification = 1;
    }

    context = gdk_pango_context_get_for_screen(gdk_screen_get_default());
    layout = pango_layout_new(context);
    pango_layout_set_font_description(layout, priv->major_font);

    if (priv->is_logarithmic) {
        pmjt = &g_array_index(priv->mjticks, GwyGraphAxisTick, 0);
        if (pmjt->value < -4 || pmjt->value > 3)
            human_fmt = FALSE;
        pmjt = &g_array_index(priv->mjticks, GwyGraphAxisTick, priv->mjticks->len-1);
        if (pmjt->value < -4 || pmjt->value > 3)
            human_fmt = FALSE;
    }

    for (i = 0; i < priv->mjticks->len; i++) {
        /* Find the value we want to put in string */
        pmjt = &g_array_index(priv->mjticks, GwyGraphAxisTick, i);
        value = pmjt->value;
        if (format)
            value /= format->magnitude;

        /* Fill tick labels dependent to mode */
        if (priv->is_logarithmic) {
            if (human_fmt)
                pmjt->text = g_strdup_printf("%g", gwy_exp10(value));
            else
                pmjt->text = g_strdup_printf("10<sup>%d</sup>", GWY_ROUND(value));
        }
        else {
            if (priv->major_printmode == GWY_GRAPH_AXIS_SCALE_FORMAT_AUTO) {
                pmjt->text = g_strdup_printf("%.*f", format->precision, value);
            }
            else if (priv->major_printmode == GWY_GRAPH_AXIS_SCALE_FORMAT_EXP) {
                if (value == 0)
                    pmjt->text = g_strdup_printf("0");
                else
                    pmjt->text = g_strdup_printf("%.1e", value);
            }
            else if (priv->major_printmode == GWY_GRAPH_AXIS_SCALE_FORMAT_INT) {
                pmjt->text = g_strdup_printf("%d", (int)(value+0.5));
            }
        }

        pango_layout_set_markup(layout, pmjt->text, -1);
        pango_layout_get_pixel_extents(layout, NULL, &rect);
        totalwidth += rect.width;
        totalheight += rect.height;
    }

    GWY_FREE_VALUE_FORMAT(format);
    g_object_unref(layout);
    g_object_unref(context);

    /* Guess whether we dont have too many or not enough ticks */
    if (priv->position == GTK_POS_RIGHT || priv->position == GTK_POS_LEFT) {
        if (totalheight > 200)
            return 1;
        else if (priv->mjticks->len < 3)
            return -1;
    }
    else {
        if (totalwidth > 200)
            return 1;
        else if (priv->mjticks->len < 3)
            return -1;
    }

    return 0;
}

static GwyValueFormat*
calculate_format(GwyGraphAxis *axis,
                 GwyValueFormat *format)
{
    GwyGraphAxisPrivate *priv = axis->priv;
    GwyGraphAxisTick *pmjt, *mji, *mjx;
    gdouble average, step;
    GString *u1, *u2;
    gboolean ok;
    guint i;

    /* FIXME: Does anyone really care? */
    if (priv->is_logarithmic)
        return gwy_unit_get_format(priv->unit, GWY_UNIT_FORMAT_MARKUP, 0, format);

    mji = &g_array_index(priv->mjticks, GwyGraphAxisTick, 0);
    mjx = &g_array_index(priv->mjticks, GwyGraphAxisTick, priv->mjticks->len - 1);
    average = (fabs(mjx->value) + fabs(mji->value))/2;
    step = fabs(mjx->value - mji->value);
    average = MAX(average, step);
    step /= MAX(priv->mjticks->len - 1, 1);

    format = gwy_unit_get_format_with_resolution(priv->unit, GWY_UNIT_FORMAT_MARKUP, average, step, format);

    /* Ensure the format is not too precise, but do not attempt this in weird
     * cases */
    if (priv->mjticks->len < 2)
        return format;

    format->precision++;
    u1 = g_string_new(NULL);
    u2 = g_string_new(NULL);
    ok = TRUE;
    while (ok && format->precision > 0 && priv->mjticks->len > 1) {
        gdouble v0;

        format->precision--;
        ok = TRUE;
        pmjt = &g_array_index(priv->mjticks, GwyGraphAxisTick, 0);
        v0 = pmjt->value/format->magnitude;
        g_string_printf(u1, "%.*f", format->precision, v0);

        for (i = 1; i < 6; i++) {
            gdouble v;

            if (i < priv->mjticks->len) {
                pmjt = &g_array_index(priv->mjticks, GwyGraphAxisTick, i);
                v = pmjt->value/format->magnitude;
            }
            else {
                pmjt = &g_array_index(priv->mjticks, GwyGraphAxisTick, priv->mjticks->len - 1);
                v = (pmjt->value/format->magnitude - v0)/(priv->mjticks->len - 1)*i + v0;
            }
            g_string_printf(u2, "%.*f", format->precision, v);
            if (gwy_strequal(u2->str, u1->str)) {
                format->precision++;
                ok = FALSE;
                break;
            }
            GWY_SWAP(GString*, u2, u1);
        }
    }

    g_string_free(u1, TRUE);
    g_string_free(u2, TRUE);

    return format;
}

/* FIXME GTK3 this is not actually used ever. Remove? */
/**
 * gwy_graph_axis_set_auto:
 * @axis: An axis.
 * @is_auto: %TRUE to enable automatic tick size and distribution adjustment, %FALSE to disable it.
 *
 * Enables or disables automatic axis adjustmet.
 **/
void
gwy_graph_axis_set_auto(GwyGraphAxis *axis, gboolean is_auto)
{
    g_return_if_fail(GWY_IS_GRAPH_AXIS(axis));

    GwyGraphAxisPrivate *priv = axis->priv;
    priv->is_auto = is_auto;
    adjust(axis);
}

/**
 * gwy_graph_axis_request_range:
 * @axis: An axis.
 * @min: Minimum requisition (min boundary value).
 * @max: Maximum requisition (max boundary value).
 *
 * Sets the requisition of axis boundaries.
 *
 * The axis will adjust the boundaries to satisfy requisition but still have reasonable tick values and spacing.  Use
 * gwy_graph_axis_get_range() to obtain the boundaries the axis actually decided to use.
 **/
void
gwy_graph_axis_request_range(GwyGraphAxis *axis, gdouble min, gdouble max)
{
    g_return_if_fail(GWY_IS_GRAPH_AXIS(axis));

    GwyGraphAxisPrivate *priv = axis->priv;
    if (priv->reqmin == min && priv->reqmax == max)
        return;

    priv->reqmin = min;
    priv->reqmax = max;

    adjust(axis);
    if (gtk_widget_get_realized(GTK_WIDGET(axis)))
        gtk_widget_queue_resize(GTK_WIDGET(axis));
}

/**
 * gwy_graph_axis_get_range:
 * @axis: An axis.
 * @min: Location to store actual axis minimum, or %NULL.
 * @max: Location to store actual axis maximum, or %NULL.
 *
 * Gets the actual boundaries of an axis.
 **/
void
gwy_graph_axis_get_range(GwyGraphAxis *axis,
                         gdouble *min,
                         gdouble *max)
{
    g_return_if_fail(GWY_IS_GRAPH_AXIS(axis));

    GwyGraphAxisPrivate *priv = axis->priv;
    if (min)
        *min = priv->min;
    if (max)
        *max = priv->max;
}

/**
 * gwy_graph_axis_get_requested_range:
 * @axis: An axis.
 * @min: Location to store requested axis minimum, or %NULL.
 * @max: Location to store requested axis maximum, or %NULL.
 *
 * Gets the requested boundaries of an axis.
 **/
void
gwy_graph_axis_get_requested_range(GwyGraphAxis *axis,
                                   gdouble *min,
                                   gdouble *max)
{
    g_return_if_fail(GWY_IS_GRAPH_AXIS(axis));

    GwyGraphAxisPrivate *priv = axis->priv;
    if (min)
        *min = priv->reqmin;
    if (max)
        *max = priv->reqmax;
}

/**
 * gwy_graph_axis_set_label:
 * @axis: An axis.
 * @label: The new label text (it can be %NULL for an empty label).
 *
 * Sets the label text of an axis.
 **/
void
gwy_graph_axis_set_label(GwyGraphAxis *axis,
                         const gchar *label)
{
    g_return_if_fail(GWY_IS_GRAPH_AXIS(axis));

    if (!label)
        label = "";

    gwy_debug("label_text = <%s>", label);

    GwyGraphAxisPrivate *priv = axis->priv;
    if (gwy_strequal(label, priv->label_text->str))
        return;

    g_string_assign(priv->label_text, label);
    g_object_notify_by_pspec(G_OBJECT(axis), properties[PROP_LABEL]);
}

/**
 * gwy_graph_axis_get_label:
 * @axis: An axis.
 *
 * Gets the label of an axis.
 *
 * Returns: Axis label as a string owned by @axis.
 **/
const gchar*
gwy_graph_axis_get_label(GwyGraphAxis *axis)
{
    g_return_val_if_fail(GWY_IS_GRAPH_AXIS(axis), "");
    return axis->priv->label_text->str;
}

/**
 * gwy_graph_axis_get_unit:
 * @axis: An axis.
 *
 * Obtains the axis unit.
 **/
GwyUnit*
gwy_graph_axis_get_unit(GwyGraphAxis *axis)
{
    g_return_val_if_fail(GWY_IS_GRAPH_AXIS(axis), NULL);
    return axis->priv->unit;
}

/**
 * gwy_graph_axis_enable_label_edit:
 * @axis: Axis widget
 * @enable: enable/disable user to change axis label
 *
 * Enables/disables user to change axis label by clicking on axis widget.
 **/
void
gwy_graph_axis_enable_label_edit(GwyGraphAxis *axis, gboolean enable)
{
    g_return_if_fail(GWY_IS_GRAPH_AXIS(axis));
    GwyGraphAxisPrivate *priv = axis->priv;
    priv->enable_label_edit = enable;
}

/**
 * gwy_graph_axis_get_magnification:
 * @axis: Axis widget
 *
 * Gets the magnification value of a graph axis.
 *
 * Returns: Magnification value of the axis.
 **/
gdouble
gwy_graph_axis_get_magnification(GwyGraphAxis *axis)
{
    g_return_val_if_fail(GWY_IS_GRAPH_AXIS(axis), 1.0);
    return axis->priv->magnification;
}

/* FIXME: What a silly name. It is simply the units displayed with the label, not magnification.
 * And we should simply have a function returning the value format. Then the caller can use whatever fields
 * she wants. */
/**
 * gwy_graph_axis_get_magnification_string:
 * @axis: An axis.
 *
 * Gets the magnification string of an axis.
 *
 * Returns: Magnification string of the axis, owned by the axis.
 **/
const gchar*
gwy_graph_axis_get_magnification_string(GwyGraphAxis *axis)
{
    g_return_val_if_fail(GWY_IS_GRAPH_AXIS(axis), "");
    GwyGraphAxisPrivate *priv = axis->priv;
    if (priv->magnification_string)
        return priv->magnification_string->str;
    else
        return "";
}

/**
 * gwy_graph_axis_get_major_ticks:
 * @axis: An axis.
 * @nticks: Location to store the number of returned ticks.
 *
 * Gets the positions of major ticks of an axis.
 *
 * Returns: A newly allocated array with the positions of axis major ticks (as real values, not pixels).
 **/
gdouble*
gwy_graph_axis_get_major_ticks(GwyGraphAxis *axis, guint *nticks)
{
    g_return_val_if_fail(GWY_IS_GRAPH_AXIS(axis), NULL);

    GwyGraphAxisPrivate *priv = axis->priv;
    guint n = priv->mjticks->len;
    gdouble *tickvalues = g_new(gdouble, n);

    for (guint i = 0; i < n; i++) {
        GwyGraphAxisTick *pmji = &g_array_index(priv->mjticks, GwyGraphAxisTick, i);
        if (priv->is_logarithmic)
            tickvalues[i] = gwy_exp10(pmji->value);
        else
            tickvalues[i] = pmji->value;
    }

    if (nticks)
        *nticks = n;

    return tickvalues;
}

/**
 * gwy_graph_axis_is_logarithmic:
 * @axis: An axis.
 *
 * Determines whether axis is set to be locarithmic.
 *
 * Returns: %TRUE if @axis is logarithmic.
 **/
gboolean
gwy_graph_axis_is_logarithmic(GwyGraphAxis *axis)
{
    g_return_val_if_fail(GWY_IS_GRAPH_AXIS(axis), FALSE);
    return axis->priv->is_logarithmic;
}

/**
 * gwy_graph_axis_get_position:
 * @axis: An axis.
 *
 * Gets the position of an axis.
 *
 * Returns: The position.
 **/
GtkPositionType
gwy_graph_axis_get_position(GwyGraphAxis *axis)
{
    g_return_val_if_fail(GWY_IS_GRAPH_AXIS(axis), GTK_POS_BOTTOM);
    return axis->priv->position;
}

static void
unit_changed(GwyGraphAxis *axis, G_GNUC_UNUSED GwyUnit *unit)
{
    adjust(axis);
}

/**
 * SECTION: graph-axis
 * @title: GwyGraphAxis
 * @short_description: Axis with ticks and labels
 * @see_also: #GwyColorAxis -- Axis with a false color scale,
 *            #GwyRuler -- Horizontal and vertical rulers
 *
 * #GwyGraphAxis is used for drawing axis. It is namely used within #GwyGraph widget, but it can be also used
 * standalone. It plots a horizontal or vertical axis with major and minor ticks, with ranges in the requested
 * interval.
 **/

/* FIXME GTK3 this is only used for an internal property which is always AUTO because there is not way to change
 * it. Remove? */
/**
 * GwyGraphAxisScaleFormat:
 * @GWY_GRAPH_AXIS_SCALE_FORMAT_EXP: Exponential (`scienfitic') format.
 * @GWY_GRAPH_AXIS_SCALE_FORMAT_INT: Integer format.
 * @GWY_GRAPH_AXIS_SCALE_FORMAT_AUTO: Automatical format.
 *
 * Labeled axis tick mark format.
 **/

/* vim: set cin columns=120 tw=118 et ts=4 sw=4 cino=>1s,e0,n0,f0,{0,}0,^0,\:1s,=0,g1s,h0,t0,+1s,c3,(0,u0 : */
