/*
 *  $Id: scatter.c 30141 2026-06-25 12:07:42Z yeti-dn $
 *  Copyright (C) 2011-2026 David Nečas (Yeti).
 *  E-mail: yeti@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 <glib/gi18n-lib.h>

#include "libgwyddion/macros.h"
#include "libgwyddion/math.h"
#include "libgwyddion/rand-gen-set.h"
#include "libgwyddion/serializable-utils.h"
#include "libgwyddion/scatter.h"
#include "libgwyddion/stats.h"
#include "libgwyddion/arithmetic.h"

#include "libgwyddion/omp.h"
#include "libgwyddion/internal.h"

#define TYPE_NAME "GwyScatter"

enum {
    SGNL_XY_CHANGED,
    SGNL_DATA_CHANGED,
    NUM_SIGNALS
};

enum {
    ITEM_NVALUES,
    ITEM_UNIT_XY, ITEM_UNITS_DATA,
    ITEM_XY, ITEM_DATA,
    ITEM_DATA_LABELS,
    NUM_ITEMS
};

static void             finalize              (GObject *object);
static void             serializable_init     (GwySerializableInterface *iface);
static void             serializable_itemize  (GwySerializable *serializable,
                                               GwySerializableGroup *group);
static gboolean         serializable_construct(GwySerializable *serializable,
                                               GwySerializableGroup *group,
                                               GwyErrorList **error_list);
static GwySerializable* serializable_copy     (GwySerializable *serializable);
static void             serializable_assign   (GwySerializable *destination,
                                               GwySerializable *source);
static void             copy_info             (GwyScatter *dest,
                                               GwyScatter *src);
static void             ensure_ranges         (GwyScatter *scatter);
static void             ensure_checksum       (GwyScatter *scatter);
static void             filter_points         (GwyScatter *scatter,
                                               const guint32 *keep,
                                               GwyXY *outxy,
                                               gdouble *outdata,
                                               gsize nout);

static guint signals[NUM_SIGNALS];
static GObjectClass *parent_class = NULL;

static GwySerializableItem serializable_items[NUM_ITEMS] = {
    { .name = "nvalues",     .ctype = GWY_SERIALIZABLE_INT32,         },
    { .name = "unit_xy",     .ctype = GWY_SERIALIZABLE_OBJECT,        },
    { .name = "units_data",  .ctype = GWY_SERIALIZABLE_OBJECT_ARRAY,  },
    { .name = "xy",          .ctype = GWY_SERIALIZABLE_DOUBLE_ARRAY,
                             .flags = GWY_SERIALIZABLE_BIG_DATA       },
    { .name = "data",        .ctype = GWY_SERIALIZABLE_DOUBLE_ARRAY,
                             .flags = GWY_SERIALIZABLE_BIG_DATA       },
    { .name = "data_labels", .ctype = GWY_SERIALIZABLE_STRING_ARRAY   },
};

G_DEFINE_TYPE_WITH_CODE(GwyScatter, gwy_scatter, G_TYPE_OBJECT,
                        G_ADD_PRIVATE(GwyScatter)
                        G_IMPLEMENT_INTERFACE(GWY_TYPE_SERIALIZABLE, serializable_init));

static void
serializable_init(GwySerializableInterface *iface)
{
    iface->itemize   = serializable_itemize;
    iface->construct = serializable_construct;
    iface->copy      = serializable_copy;
    iface->assign    = serializable_assign;

    serializable_items[ITEM_UNIT_XY].aux.object_type = GWY_TYPE_UNIT;
    serializable_items[ITEM_UNITS_DATA].aux.object_type = GWY_TYPE_UNIT;
}

static void
gwy_scatter_class_init(GwyScatterClass *klass)
{
    GObjectClass *gobject_class = G_OBJECT_CLASS(klass);
    GType type = G_TYPE_FROM_CLASS(klass);

    parent_class = gwy_scatter_parent_class;

    gobject_class->finalize = finalize;

    /**
     * GwyScatter::xy-changed:
     * @gwyscatter: The #GwyScatter which received the signal.
     *
     * The ::xy-changed signal is never emitted by the object itself.  It is intended as a means to notify object
     * users they should update themselves, usually when a computation modifying the XY positions is finished.
     **/
    signals[SGNL_XY_CHANGED] = g_signal_new("xy-changed", type,
                                            G_SIGNAL_RUN_FIRST,
                                            G_STRUCT_OFFSET(GwyScatterClass, xy_changed),
                                            NULL, NULL,
                                            g_cclosure_marshal_VOID__VOID,
                                            G_TYPE_NONE, 0);
    g_signal_set_va_marshaller(signals[SGNL_XY_CHANGED], type, g_cclosure_marshal_VOID__VOIDv);

    /**
     * GwyScatter::data-changed:
     * @gwyscatter: The #GwyScatter which received the signal.
     *
     * The ::data-changed signal is never emitted by the object itself.  It is intended as a means to notify object
     * users they should update themselves, usually when a computation modifying the data is finished.
     **/
    signals[SGNL_DATA_CHANGED] = g_signal_new("data-changed", type,
                                              G_SIGNAL_RUN_FIRST,
                                              G_STRUCT_OFFSET(GwyScatterClass, data_changed),
                                              NULL, NULL,
                                              g_cclosure_marshal_VOID__VOID,
                                              G_TYPE_NONE, 0);
    g_signal_set_va_marshaller(signals[SGNL_DATA_CHANGED], type, g_cclosure_marshal_VOID__VOIDv);
}

static void
gwy_scatter_init(GwyScatter *scatter)
{
    scatter->priv = gwy_scatter_get_instance_private(scatter);
}

static inline void
set_default_data_labels(gchar **labels, gint pos, gint len)
{
    for (gint i = pos; i < pos+len; i++)
        labels[i] = g_strdup_printf("%s %d", _("Value"), i+1);
}

static void
alloc_data(GwyScatter *scatter, gsize n, gint nvalues)
{
    GwyScatterPrivate *priv = scatter->priv;

    if (priv->nvalues != nvalues) {
        if (priv->nvalues > nvalues) {
            for (gint i = nvalues; i < priv->nvalues; i++)
                g_clear_object(priv->units_data + i);
        }
        priv->units_data = g_renew(GwyUnit*, priv->units_data, nvalues);
        if (priv->nvalues < nvalues)
            gwy_clear(priv->units_data + priv->nvalues, nvalues - priv->nvalues);

        // We promise to keep the data labels, similarly to units, even though it is only useful sometimes and other
        // times it is extra work.
        if (priv->datalabels) {
            for (gint i = nvalues; i < priv->nvalues; i++)
                g_free(priv->datalabels[i]);
            priv->datalabels = g_renew(gchar*, priv->datalabels, nvalues);
            set_default_data_labels(priv->datalabels, priv->nvalues, nvalues - priv->nvalues);
        }

        priv->valuemin = g_renew(gdouble, priv->valuemin, nvalues);
        priv->valuemax = g_renew(gdouble, priv->valuemax, nvalues);
    }

    if (scatter->n != n) {
        GWY_FREE(priv->xy);
        if (n)
            priv->xy = g_new(GwyXY, n);
    }

    if (priv->nvalues != nvalues || scatter->n != n) {
        GWY_FREE(priv->data);
        if (n && nvalues)
            priv->data = g_new(gdouble, n*nvalues);
    }

    priv->nvalues = nvalues;
    scatter->n = n;
}

static void
finalize(GObject *object)
{
    GwyScatter *scatter = GWY_SCATTER(object);
    GwyScatterPrivate *priv = scatter->priv;

    if (priv->checksummer) {
        g_checksum_free(priv->checksummer);
        priv->checksummer = NULL;
    }
    g_clear_object(&priv->unit_xy);
    alloc_data(scatter, 0, 0);

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

static void
copy_info(GwyScatter *dest, GwyScatter *src)
{
    gwy_scatter_copy_units(src, dest);
    guint snvalues = src->priv->nvalues;
    assign_string_array(&dest->priv->datalabels, snvalues, src->priv->datalabels, snvalues);
}

/**
 * gwy_scatter_new: (constructor)
 * @nvalues: The number of data values per point (excluding XY coordinates).
 *
 * Creates a new empty scattered data.
 *
 * The scattered data will not contain any points. Usually, gwy_scatter_new_from_data() is more useful.
 *
 * Returns: (transfer full):
 *          A newly created scattered data.
 **/
GwyScatter*
gwy_scatter_new(gint nvalues)
{
    GwyScatter *scatter = (GwyScatter*)g_object_new(GWY_TYPE_SCATTER, NULL);
    g_return_val_if_fail(nvalues >= 0, scatter);
    alloc_data(scatter, 0, nvalues);
    return scatter;
}

/**
 * gwy_scatter_new_sized:
 * @nvalues: The number of data values per point (excluding XY coordinates).
 * @n: Number of points.
 *
 * Creates a new scattered data with a preallocated size.
 *
 * The scattered data will contain the speficied number of points with uninitialised (undefined) content.
 *
 * Returns: (transfer full):
 *          A newly created scattered data.
 **/
GwyScatter*
gwy_scatter_new_sized(gint nvalues,
                      gsize n)
{
    GwyScatter *scatter = g_object_new(GWY_TYPE_SCATTER, NULL);
    g_return_val_if_fail(nvalues >= 0, scatter);
    alloc_data(scatter, n, nvalues);
    return scatter;
}

/**
 * gwy_scatter_new_from_xyz:
 * @xyz: (array length=n): Array of @n points with the scattered data.
 * @n: Number of points.
 *
 * Creates a new single-value scattered data, filling it with provided XYZ data.
 *
 * The X and Y parts of @xyz will become the coordinatres and Z will become the value.
 *
 * Returns: (transfer full):
 *          A newly created scattered data.
 **/
GwyScatter*
gwy_scatter_new_from_xyz(const GwyXYZ *xyz,
                         gsize n)
{
    GwyScatter *scatter = g_object_new(GWY_TYPE_SCATTER, NULL);
    g_return_val_if_fail(!n || xyz, scatter);
    alloc_data(scatter, n, 1);

    GwyXY *xy = scatter->priv->xy;
    gdouble *data = scatter->priv->data;
    for (gsize i = 0; i < n; i++) {
        xy[i].x = xyz[i].x;
        xy[i].y = xyz[i].y;
        data[i] = xyz[i].z;
    }
    return scatter;
}

/**
 * gwy_scatter_new_from_data:
 * @points: Array of @n XY coordinates for the scattered data.
 * @data: Array of @n×@nvalues data values with the data values.
 * @nvalues: The number of data values per point (excluding XY coordinates).
 * @n: Number of points.
 *
 * Creates a new single-value scattered data, filling it with provided coordinates and data.
 *
 * The data are passed as a single flat array, indexed first by value (outer index), then by point (inner index).
 *
 * Returns: (transfer full):
 *          A newly created scattered data.
 **/
GwyScatter*
gwy_scatter_new_from_data(const GwyXY *points,
                          const gdouble *data,
                          gint nvalues,
                          gsize n)
{
    GwyScatter *scatter = g_object_new(GWY_TYPE_SCATTER, NULL);
    g_return_val_if_fail(nvalues >= 0, scatter);
    g_return_val_if_fail(!n || points, scatter);
    g_return_val_if_fail(!(n && nvalues) || data, scatter);
    alloc_data(scatter, n, nvalues);

    gwy_assign(scatter->priv->xy, points, n);
    gwy_assign(scatter->priv->data, data, n*nvalues);

    return scatter;
}

/**
 * gwy_scatter_new_alike:
 * @model: A scattered data to use as the template.
 *
 * Creates a new empty scattered data similar to another scattered data.
 *
 * The number of values per point and their units of the new scattered data will be identical to those of @model but
 * the new scattered data will not contain any points. Use gwy_scatter_copy() to completely duplicate scattered data
 * including points and values.
 *
 * Returns: (transfer full):
 *          A newly created scattered data.
 **/
GwyScatter*
gwy_scatter_new_alike(GwyScatter *model)
{
    GwyScatter *scatter = g_object_new(GWY_TYPE_SCATTER, NULL);
    g_return_val_if_fail(GWY_IS_SCATTER(model), scatter);
    alloc_data(scatter, 0, model->priv->nvalues);
    copy_info(scatter, model);
    return scatter;
}

/**
 * gwy_scatter_get_n_values:
 * @scatter: A scattered data.
 *
 * Reports the number of values per point in scattered data.
 *
 * Returns: The number of values per point.
 **/
gint
gwy_scatter_get_n_values(GwyScatter *scatter)
{
    g_return_val_if_fail(GWY_IS_SCATTER(scatter), 0);
    return scatter->priv->nvalues;
}

/**
 * gwy_scatter_get_xy:
 * @scatter: A scattered data.
 *
 * Gets one raw value array of a scattered data.
 *
 * The returned array contains gwy_scatter_get_n_points() data values which can directly modified. It is owned by
 * @scatter and is not guaranteed to be valid through it data life time, only until it changes.
 *
 * This function invalidates any cached information, use gwy_scatter_get_xy_const() if you are not going to change
 * the data. See gwy_scatter_invalidate() for some discussion.
 *
 * Returns: The array with XY point coordinates.
 **/
GwyXY*
gwy_scatter_get_xy(GwyScatter *scatter)
{
    g_return_val_if_fail(GWY_IS_SCATTER(scatter), NULL);
    gwy_scatter_invalidate(scatter);
    return scatter->priv->xy;
}

/**
 * gwy_scatter_get_xy_const:
 * @scatter: A scattered data.
 *
 * Gets one raw value array of a scattered data, read-only.
 *
 * The returned array contains gwy_scatter_get_n_points() coordinates. It is owned by @scatter and is not guaranteed
 * to be valid through it data life time, only until it changes.
 *
 * Use gwy_scatter_get_xy() if you want to to change the data. See gwy_scatter_invalidate() for some discussion.
 *
 * Returns: The array with XY point coordinates.
 **/
const GwyXY*
gwy_scatter_get_xy_const(GwyScatter *scatter)
{
    g_return_val_if_fail(GWY_IS_SCATTER(scatter), NULL);
    return scatter->priv->xy;
}

/**
 * gwy_scatter_get_data:
 * @scatter: A scattered data.
 * @i: Index of a value in @scatter.
 *
 * Gets one raw value array of a scattered data.
 *
 * The returned array contains gwy_scatter_get_n_points() data values which can directly modified. It is owned by
 * @scatter and is not guaranteed to be valid through it data life time, only until it changes.
 *
 * This function invalidates any cached information, use gwy_scatter_get_data_const() if you are not going to change
 * the data. See gwy_scatter_invalidate() for some discussion.
 *
 * Returns: The array with @i-th scattered data values.
 **/
gdouble*
gwy_scatter_get_data(GwyScatter *scatter,
                     gint i)
{
    g_return_val_if_fail(GWY_IS_SCATTER(scatter), NULL);
    g_return_val_if_fail(i >= 0 && i < scatter->priv->nvalues, NULL);
    gwy_scatter_invalidate(scatter);
    return scatter->priv->data + i*scatter->n;
}

/**
 * gwy_scatter_get_data_const:
 * @scatter: A scattered data.
 * @i: Index of a value in @scatter.
 *
 * Gets one raw value array of a scattered data, read-only.
 *
 * The returned array contains gwy_scatter_get_n_points() data values. It is owned by @scatter and is not guaranteed
 * to be valid through it data life time, only until it changes.
 *
 * Use gwy_scatter_get_data() if you want to to change the data. See gwy_scatter_invalidate() for some discussion.
 *
 * Returns: The array with @i-th scattered data values.
 **/
const gdouble*
gwy_scatter_get_data_const(GwyScatter *scatter,
                           gint i)
{
    g_return_val_if_fail(GWY_IS_SCATTER(scatter), NULL);
    g_return_val_if_fail(i >= 0 && i < scatter->priv->nvalues, NULL);
    return scatter->priv->data + i*scatter->n;
}

/**
 * gwy_scatter_pack:
 * @scatter: A scattered data.
 * @i: Index of a value in @scatter, or -1 for all values.
 *
 * Packs scattered data by point into a new array.
 *
 * The scattered data are normally organised by value planes. If you need to transform them to a form organised by
 * point, this function does such transformation.
 *
 * When @i is non-negative, the returned flat array consists of value triplets x, y, v_i. When @i is negative, the
 * array always contains x, y and then all the values for the correspoinding point.
 *
 * See also gwy_scatter_set_from_packed() which takes data in the packed format a uses them to fill @scatter.
 *
 * Returns: (transfer full): The array with packed data values, to be freed with g_free().
 **/
gdouble*
gwy_scatter_pack(GwyScatter *scatter,
                 gint i)
{
    g_return_val_if_fail(GWY_IS_SCATTER(scatter), NULL);
    GwyScatterPrivate *priv = scatter->priv;
    gint nvalues = priv->nvalues;
    g_return_val_if_fail(i < nvalues, NULL);

    const GwyXY *xy = priv->xy;
    const gdouble *data = priv->data;
    gsize n = scatter->n;
    gdouble *retval;
    if (i < 0) {
        retval = g_new(gdouble, n*(nvalues + 2));
        for (gsize k = 0; k < n; k++) {
            gdouble *block = retval + k*(nvalues + 2);
            block[0] = xy[k].x;
            block[1] = xy[k].y;
            for (gint m = 0; m < nvalues; m++)
                block[2 + m] = data[m*n + k];
        }
    }
    else {
        retval = g_new(gdouble, n*3);
        data += i*n;
        for (gsize k = 0; k < n; k++) {
            gdouble *block = retval + k*3;
            block[0] = xy[k].x;
            block[1] = xy[k].y;
            block[2] = data[k];
        }
    }
    return retval;
}

/**
 * gwy_scatter_get_n_points:
 * @scatter: A scattered data.
 *
 * Gets the number of points in a scattered data.
 *
 * Returns: The number of points.
 **/
gsize
gwy_scatter_get_n_points(GwyScatter *scatter)
{
    g_return_val_if_fail(GWY_IS_SCATTER(scatter), 0);
    return scatter->n;
}

/**
 * gwy_scatter_data_changed:
 * @scatter: A scattered data.
 *
 * Emits signal GwyScatter::data-changed on a scattered data.
 *
 * If you change both coordinates and data, you should also emit GwyScatter::xy-changed.
 **/
void
gwy_scatter_data_changed(GwyScatter *scatter)
{
    g_return_if_fail(GWY_IS_SCATTER(scatter));
    g_signal_emit(scatter, signals[SGNL_DATA_CHANGED], 0);
}

/**
 * gwy_scatter_xy_changed:
 * @scatter: A scattered data.
 *
 * Emits signal GwyScatter::xy-changed on a scattered data.
 *
 * If you change both coordinates and data, you should also emit GwyScatter::data-changed.
 **/
void
gwy_scatter_xy_changed(GwyScatter *scatter)
{
    g_return_if_fail(GWY_IS_SCATTER(scatter));
    g_signal_emit(scatter, signals[SGNL_XY_CHANGED], 0);
}

/**
 * gwy_scatter_copy_data:
 * @scatter: Source scattered data.
 * @target: Destination scattered data.
 *
 * Copies the data of a scattered data to another scattered data of the same dimensions.
 *
 * Only the data points are copied. Both the number of points and number of values per point must match. To make
 * a scattered data completely identical to another, including resizing and chanding units or labels, you can use
 * gwy_scatter_assign().
 **/
void
gwy_scatter_copy_data(GwyScatter *scatter, GwyScatter *target)
{
    g_return_if_fail(GWY_IS_SCATTER(scatter));
    g_return_if_fail(GWY_IS_SCATTER(target));
    if (scatter == target)
        return;

    g_return_if_fail(target->n == scatter->n);
    GwyScatterPrivate *dpriv = target->priv, *spriv = scatter->priv;
    g_return_if_fail(dpriv->nvalues == spriv->nvalues);

    gwy_assign(dpriv->xy, spriv->xy, scatter->n);
    gwy_assign(dpriv->data, spriv->data, scatter->n*spriv->nvalues);
    gwy_scatter_invalidate(target);

    dpriv->cached_ranges = spriv->cached_ranges;
    dpriv->xymin = spriv->xymin;
    dpriv->xymax = spriv->xymax;
    dpriv->cached_checksum = spriv->cached_checksum;
    gwy_assign(dpriv->checksum, spriv->checksum, G_N_ELEMENTS(spriv->checksum));
}

/**
 * gwy_scatter_invalidate:
 * @scatter: A scattered data.
 *
 * Invalidates cached scattered data statistics.
 *
 * Cached statistics include ranges returned by gwy_scatter_xrange(), gwy_scatter_yrange() and gwy_scatter_min_max(),
 * the fingerprint for gwy_scatter_xy_is_compatible() and and possibly other characteristics in the future.
 *
 * See gwy_field_invalidate() for discussion of invalidation and examples.
 **/
void
gwy_scatter_invalidate(GwyScatter *scatter)
{
    g_return_if_fail(GWY_IS_SCATTER(scatter));
    scatter->priv->cached_ranges = FALSE;
    scatter->priv->cached_checksum = FALSE;
}

/**
 * gwy_scatter_set_data_label:
 * @scatter: A scattered data.
 * @i: Index of a value in @scatter.
 * @label: New value label.
 *
 * Sets the label of a value in scattered data.
 *
 * All values can be either unlabelled or labelled. When they are unlabelled and you set the first label, all others
 * automatically get a label (‘Unknown’, ‘Value 1’ or similar).
 **/
void
gwy_scatter_set_data_label(GwyScatter *scatter,
                           gint i,
                           const gchar *label)
{
    g_return_if_fail(GWY_IS_SCATTER(scatter));
    GwyScatterPrivate *priv = scatter->priv;
    g_return_if_fail(i >= 0 && i < priv->nvalues);

    if (!priv->datalabels) {
        priv->datalabels = g_new0(gchar*, priv->nvalues);
        for (gint j = 0; j < priv->nvalues; j++) {
            if (j != i)
                set_default_data_labels(priv->datalabels, j, 1);
        }
    }
    gwy_assign_string(priv->datalabels + i, label);
}

/**
 * gwy_scatter_get_data_label:
 * @scatter: A scattered data.
 * @i: Index of a value in @scatter.
 *
 * Gets the label of a value in a scattered data.
 *
 * Returns: (nullable): Value label, as a string owned by @scatter.  It may be %NULL.
 **/
const gchar*
gwy_scatter_get_data_label(GwyScatter *scatter,
                           gint i)
{
    g_return_val_if_fail(GWY_IS_SCATTER(scatter), NULL);
    GwyScatterPrivate *priv = scatter->priv;
    g_return_val_if_fail(i >= 0 && i < priv->nvalues, NULL);

    if (!priv->datalabels)
        return NULL;

    return priv->datalabels[i];
}

/**
 * gwy_scatter_find_data_by_label:
 * @scatter: A scattered data.
 * @label: (nullable): Value label to find.
 *
 * Finds the first value in scattered data matching a label.
 *
 * It is possible to pass %NULL as @label. It can find anything only if all values are unlabelled, and in such case it
 * will return the first value index (provided there are any values). Hence, it is not particularly useful.
 *
 * Returns: The value index if found, a negative value (-1) if not found.
 **/
gint
gwy_scatter_find_data_by_label(GwyScatter *scatter,
                               const gchar *label)
{
    g_return_val_if_fail(GWY_IS_SCATTER(scatter), -1);
    GwyScatterPrivate *priv = scatter->priv;
    gint nvalues = priv->nvalues;
    if (!label)
        return priv->datalabels ? -1 : 0;
    if (!priv->datalabels)
        return -1;

    for (gint i = 0; i < nvalues; i++) {
        if (gwy_strequal(priv->datalabels[i], label))
            return i;
    }
    return -1;
}

/**
 * gwy_scatter_clear_data_labels:
 * @scatter: A scattered data.
 *
 * Completely removes all data labels in a scattered data.
 *
 * The scattered data revert to a clean state when gwy_scatter_get_data_label() returns %NULL for all values. See
 * gwy_scatter_set_data_label() for a discussion.
 **/
void
gwy_scatter_clear_data_labels(GwyScatter *scatter)
{
    g_return_if_fail(GWY_IS_SCATTER(scatter));
    GwyScatterPrivate *priv = scatter->priv;
    if (priv->datalabels) {
        gint nvalues = priv->nvalues;
        for (gint i = 0; i < nvalues; i++)
            g_free(priv->datalabels[i]);
        GWY_FREE(priv->datalabels);
    }
}

/**
 * gwy_scatter_set_from_packed:
 * @scatter: A scattered data.
 * @data: Array of @n×(@nvalues + 2) data values with XY coordinates and data values.
 * @nvalues: The number of data values per point (excluding XY coordinates).
 * @n: Number of points.
 *
 * Fills a scattered data from packed values.
 *
 * The data format is the same as what gwy_scatter_pack() creates, i.e. it is grouped by point, not by value planes.
 * For single-valued data, you can also type-cast #GwyXYZ to #gdouble* and pass it as @data.
 *
 * The scattered data object is resized to match @nvalues and @n. Units and labels are kept (for the minimum of old
 * and new @nvalues). However, if you change the number of values, usually all the units and labels need to be
 * updated.
 **/
void
gwy_scatter_set_from_packed(GwyScatter *scatter,
                            const gdouble *data,
                            gint nvalues,
                            gsize n)
{
    g_return_if_fail(GWY_IS_SCATTER(scatter));
    g_return_if_fail(!n || !nvalues || data);
    alloc_data(scatter, n, nvalues);

    GwyXY *xy = scatter->priv->xy;
    gdouble *sdata = scatter->priv->data;
    for (gsize k = 0; k < n; k++) {
        const gdouble *block = data + (nvalues + 2)*k;
        xy[k].x = block[0];
        xy[k].y = block[1];
        for (gint m = 0; m < nvalues; m++)
            sdata[m*n + k] = block[m+2];
    }
    gwy_scatter_invalidate(scatter);
}

static void
copy_field_to_scatter(GwyField *field,
                      GwyScatter *scatter,
                      GwyNield *mask,
                      GwyMaskingType masking,
                      gint col, gint row,
                      gint width, gint height)
{
    gdouble dx = gwy_field_get_dx(field), dy = gwy_field_get_dy(field);
    gdouble xoff = 0.5*dx + field->xoff, yoff = 0.5*dy + field->yoff;
    GwyScatterPrivate *priv = scatter->priv;

    gint xres = field->xres, yres = field->yres;
    gsize n = (gsize)xres * (gsize)yres;
    const gdouble *datapos = field->priv->data + xres*row + col;
    const gint *maskpos = NULL;
    if (mask) {
        maskpos = mask->priv->data + xres*row + col;
        n = gwy_nield_area_count(mask, NULL, GWY_MASK_IGNORE, col, row, width, height);
    }

    alloc_data(scatter, n, 1);

    GwyXY *xy = priv->xy;
    gdouble *data = priv->data;
    gsize k = 0;
    for (gint i = 0; i < height; i++) {
        for (gint j = 0; j < width; j++) {
            if (nielded_included(maskpos + i*xres + j, masking)) {
                xy[k].x = dx*j + xoff;
                xy[k].y = dy*i + yoff;
                data[k] = datapos[i*xres + j];
                k++;
            }
        }
    }

    gwy_field_copy_units_to_scatter(field, scatter, 0);
    gwy_scatter_invalidate(scatter);
}

// FIXME: We may need a multi-field version of this function, filling multiple channels simultaneously using multiple
// Field. It can be done piecemeal, but it is messy, especially with masking, as a public function. Doing it in on
// step ensures everything is consistent.

/**
 * gwy_scatter_set_from_field:
 * @scatter: A scattered data.
 * @field: A two-dimensional data field.
 *
 * Fills the data of a scattered data from a data field.
 *
 * The number of points in the new scattered data will be equal to the number of points in the field.  Lateral
 * coordinates will be equal to the corresponding @field coordinates; values will be created in regular grid according
 * to @field's physical size and offset.
 *
 * Lateral and value units will correspond to @field's units.  This means the field needs to have identical @x
 * and @y units.
 **/
void
gwy_scatter_set_from_field(GwyScatter *scatter,
                           GwyField *field)
{
    g_return_if_fail(GWY_IS_SCATTER(scatter));
    g_return_if_fail(GWY_IS_FIELD(field));
    copy_field_to_scatter(field, scatter, NULL, GWY_MASK_IGNORE, 0, 0, field->xres, field->yres);
}

/**
 * gwy_scatter_set_from_field_area:
 * @scatter: A scattered data.
 * @field: A two-dimensional data field.
 * @mask: (nullable): Mask of pixels to include from/exclude, or %NULL
 * @masking: Masking mode to use.
 * @col: Column index.
 * @row: Row index.
 * @width: Area width (number of columns).
 * @height: Area height (number of rows)
 *
 * Fills the data of a scattered data from a data field, possibly using masking.
 **/
void
gwy_scatter_set_from_field_area(GwyScatter *scatter,
                                GwyField *field,
                                GwyNield *mask,
                                GwyMaskingType masking,
                                gint col,
                                gint row,
                                gint width,
                                gint height)
{
    g_return_if_fail(GWY_IS_SCATTER(scatter));
    if (!_gwy_field_check_area(field, col, row, width, height, FALSE)
        || !_gwy_field_check_mask(field, &mask, &masking))
        return;
    copy_field_to_scatter(field, scatter, mask, masking, col, row, width, height);
}

/**
 * gwy_scatter_get_unit_xy:
 * @scatter: A scattered data.
 *
 * Returns lateral SI unit of a scattered data.
 *
 * The returned object can be modified to change the scattered data coordinate units.
 *
 * Returns: (transfer none):
 *          SI unit corresponding to the lateral (XY) dimensions of the scattered data. Its reference count is not
 *          incremented.
 **/
GwyUnit*
gwy_scatter_get_unit_xy(GwyScatter *scatter)
{
    g_return_val_if_fail(GWY_IS_SCATTER(scatter), NULL);

    GwyScatterPrivate *priv = scatter->priv;
    if (!priv->unit_xy)
        priv->unit_xy = gwy_unit_new(NULL);

    return priv->unit_xy;
}

/**
 * gwy_scatter_get_unit_data:
 * @scatter: A scattered data.
 * @i: Index of a value in @scatter.
 *
 * Returns a value SI unit of a scattered data.
 *
 * The returned object can be modified to change the scattered data value units.
 *
 * Returns: (transfer none):
 *          SI unit corresponding to the @i-th value of the scattered data. Its reference count is not incremented.
 **/
GwyUnit*
gwy_scatter_get_unit_data(GwyScatter *scatter,
                          gint i)
{
    g_return_val_if_fail(GWY_IS_SCATTER(scatter), NULL);
    GwyScatterPrivate *priv = scatter->priv;
    g_return_val_if_fail(i >= 0 && i < priv->nvalues, NULL);

    if (!priv->units_data[i])
        priv->units_data[i] = gwy_unit_new(NULL);

    return priv->units_data[i];
}

/**
 * gwy_scatter_get_value_format_xy:
 * @scatter: A scattered data.
 * @style: Unit format style.
 * @format: (nullable): A SI value format to modify, or %NULL to allocate a new one.
 *
 * Finds value format good for displaying coordinates of a scattered data.
 *
 * Returns: The value format.  If @format is %NULL, a newly allocated format is returned, otherwise (modified) @format
 *          itself is returned.
 **/
GwyValueFormat*
gwy_scatter_get_value_format_xy(GwyScatter *scatter,
                                GwyUnitFormatStyle style,
                                GwyValueFormat *format)
{
    g_return_val_if_fail(GWY_IS_SCATTER(scatter), NULL);

    GwyScatterPrivate *priv = scatter->priv;
    GwyUnit *unit = priv->unit_xy;
    if (!scatter->n)
        return gwy_unit_get_format_with_resolution(unit, style, 1.0, 0.1, format);

    if (scatter->n == 1) {
        gdouble max = MAX(fabs(priv->xy[0].x), fabs(priv->xy[0].y));
        if (!max)
            max = 1.0;
        return gwy_unit_get_format_with_resolution(unit, style, max, max/10.0, format);
    }

    gdouble xmin, xmax, ymin, ymax;
    gwy_scatter_xrange(scatter, &xmin, &xmax);
    gwy_scatter_yrange(scatter, &ymin, &ymax);
    gdouble max = MAX(MAX(fabs(xmax), fabs(xmin)), MAX(fabs(ymax), fabs(ymin)));
    if (!max)
        max = 1.0;
    gdouble step = hypot(ymax - ymin, xmax - xmin)/sqrt(scatter->n);
    if (!step)
        step = max/10.0;

    return gwy_unit_get_format_with_resolution(unit, style, max, 0.2*step, format);
}

/**
 * gwy_scatter_get_value_format_data:
 * @scatter: A scattered data.
 * @i: Index of a value in @scatter.
 * @style: Unit format style.
 * @format: (nullable): A SI value format to modify, or %NULL to allocate a new one.
 *
 * Finds value format good for displaying values of a scattered data.
 *
 * Returns: The value format.  If @format is %NULL, a newly allocated format is returned, otherwise (modified) @format
 *          itself is returned.
 **/
GwyValueFormat*
gwy_scatter_get_value_format_data(GwyScatter *scatter,
                                  gint i,
                                  GwyUnitFormatStyle style,
                                  GwyValueFormat *format)
{
    g_return_val_if_fail(GWY_IS_SCATTER(scatter), NULL);
    GwyScatterPrivate *priv = scatter->priv;
    g_return_val_if_fail(i >= 0 && i < priv->nvalues, NULL);

    GwyUnit *unit = gwy_scatter_get_unit_data(scatter, i);
    gdouble max, min;
    gwy_scatter_min_max(scatter, i, &min, &max);
    if (max == min) {
        max = ABS(max);
        min = 0.0;
    }

    return gwy_unit_get_format_with_digits(unit, style, max - min, 3, format);
}

/**
 * gwy_scatter_copy_units:
 * @scatter: A scattered data.
 * @target: Target scattered data with the same number of data values.
 *
 * Sets lateral and value units of a scattered data to match another scattered data.
 **/
void
gwy_scatter_copy_units(GwyScatter *scatter,
                       GwyScatter *target)
{
    g_return_if_fail(GWY_IS_SCATTER(scatter));
    g_return_if_fail(GWY_IS_SCATTER(target));
    GwyScatterPrivate *dpriv = target->priv, *spriv = scatter->priv;
    g_return_if_fail(dpriv->nvalues == spriv->nvalues);

    _gwy_copy_unit(spriv->unit_xy, &dpriv->unit_xy);
    for (gint i = 0; i < spriv->nvalues; i++)
        _gwy_copy_unit(spriv->units_data[i], dpriv->units_data + i);
}

/**
 * gwy_field_copy_units_to_scatter:
 * @field: A two-dimensional data field.
 * @scatter: A scattered data.
 * @i: Index of a value in @scatter. It can be negative to copy only lateral units.
 *
 * Sets lateral and value units of a scattered data to match a data field.
 *
 * Latera units are set always. Value index @i determines which value units to copy from the field.
 **/
void
gwy_field_copy_units_to_scatter(GwyField *field,
                                GwyScatter *scatter,
                                gint i)
{
    g_return_if_fail(GWY_IS_SCATTER(scatter));
    g_return_if_fail(GWY_IS_FIELD(field));
    GwyScatterPrivate *priv = scatter->priv;
    g_return_if_fail(i < priv->nvalues);

    GwyFieldPrivate *fpriv = field->priv;
    _gwy_copy_unit(fpriv->unit_xy, &priv->unit_xy);
    if (i >= 0)
        _gwy_copy_unit(fpriv->unit_z, priv->units_data + i);
}

/**
 * gwy_scatter_copy_units_to_field:
 * @scatter: A scattered data.
 * @i: Index of a value in @scatter. It can be negative to only copy lateral units.
 * @field: A two-dimensional data field.
 *
 * Sets lateral and value units of a data field to match a scattered data.
 *
 * Value index @i determines which value units to copy to the field.
 **/
void
gwy_scatter_copy_units_to_field(GwyScatter *scatter,
                                gint i,
                                GwyField *field)
{
    g_return_if_fail(GWY_IS_SCATTER(scatter));
    g_return_if_fail(GWY_IS_FIELD(field));
    GwyScatterPrivate *priv = scatter->priv;
    g_return_if_fail(i < priv->nvalues);

    GwyFieldPrivate *fpriv = field->priv;
    _gwy_copy_unit(priv->unit_xy, &fpriv->unit_xy);
    if (i >= 0)
        _gwy_copy_unit(priv->units_data[i], &fpriv->unit_z);
}

/**
 * gwy_scatter_get_xy_full:
 * @scatter: A scattered data.
 * @n: (out): Location to store the number of extracted values (twice the number of data points).
 *
 * Provides the values of an entire coordinates of scattered data as a flat array.
 *
 * This function, paired with gwy_scatter_set_xy_full() can be namely useful in language bindings. In C,
 * gwy_scatter_get_xy_const() or gwy_scatter_get_xy() is usually fine.
 *
 * Note that this function returns a pointer directly to @scatter's XY data, as a flat array with alternating X and
 * Y values.
 *
 * Returns: (array length=n): The array containing the scattered data point coordinates.
 **/
const gdouble*
gwy_scatter_get_xy_full(GwyScatter *scatter,
                        gsize *n)
{
    g_return_val_if_fail(GWY_IS_SCATTER(scatter), NULL);
    *n = 2*scatter->n;
    return (const gdouble*)scatter->priv->xy;
}

/**
 * gwy_scatter_set_xy_full:
 * @scatter: A scattered data.
 * @points: (array length=n):
 *          Coordinates to copy to the scattered data.  They will replace whatever points are in @scatter now.
 * @n: The number of values in @data. The number must be even since every point has two coordinates, X and Y.
 *
 * Puts back coordinates from a flat array to scattered data.
 *
 * If the number of points changes, all data become undefined.
 *
 * This function, paired with gwy_scatter_get_xy_full() can be namely useful in language bindings. In C, you can just
 * use gwy_scatter_set_xy() and gwy_assign().
 *
 * See gwy_scatter_get_xy_full() for a discussion.
 **/
void
gwy_scatter_set_xy_full(GwyScatter *scatter,
                        const gdouble *points,
                        gsize n)
{
    g_return_if_fail(GWY_IS_SCATTER(scatter));
    g_return_if_fail(points || !n);
    g_return_if_fail(n % 2 == 0);
    GwyScatterPrivate *priv = scatter->priv;
    n /= 2;
    alloc_data(scatter, n, priv->nvalues);
    if (n)
        gwy_assign(priv->xy, (const GwyXY*)points, n);
    gwy_scatter_invalidate(scatter);
}

/**
 * gwy_scatter_get_data_full:
 * @scatter: A scattered data.
 * @i: Index of a value in @scatter.
 * @n: (out): Location to store the total count of extracted data values (which is the product of the number of points
 *            and the number of values per point).
 *
 * Provides all the data for a single value in scattered data as a flat array.
 *
 * This function, paired with gwy_scatter_set_data_full() can be namely useful in language bindings. In C,
 * gwy_scatter_get_data_const() or gwy_scatter_get_data() is usually fine.
 *
 * Note that this function returns a pointer directly to @scatter's data.
 *
 * Returns: (array length=n): The array containing the scattered data points.
 **/
const gdouble*
gwy_scatter_get_data_full(GwyScatter *scatter,
                          gint i,
                          gsize *n)
{
    g_return_val_if_fail(GWY_IS_SCATTER(scatter), NULL);
    GwyScatterPrivate *priv = scatter->priv;
    g_return_val_if_fail(i >= 0 && i < priv->nvalues, NULL);

    *n = scatter->n;
    return priv->data + i*scatter->n;
}

/**
 * gwy_scatter_set_data_full:
 * @scatter: A scattered data.
 * @i: Index of a value in @scatter.
 * @data: (array length=n): Data values to copy into the scattered data.  They will replace whatever values are in
 *                          @scatter now.
 * @n: The number of values in @data. It must be equal to the product of the number of points. Unlike
 *     gwy_scatter_set_xy_full(), this function never resizes the scattered data.
 *
 * Puts back all the data for a single value in scattered data from a flat array.
 *
 * See gwy_scatter_get_data_full() for a discussion.
 **/
void
gwy_scatter_set_data_full(GwyScatter *scatter,
                          gint i,
                          const gdouble *data,
                          gsize n)
{
    g_return_if_fail(GWY_IS_SCATTER(scatter));
    g_return_if_fail(data || !n);
    GwyScatterPrivate *priv = scatter->priv;
    g_return_if_fail(i >= 0 && i < priv->nvalues);
    g_return_if_fail(n == scatter->n);
    if (n)
        gwy_assign(priv->data + i*n, data, n);
}

/**
 * gwy_scatter_xrange:
 * @scatter: A scattered data.
 * @min: (out): Location where to store the minimum value.
 * @max: (out): Location where to store the maximum value.
 *
 * Gets the range of X coordinates of an XYZ scattered data.
 *
 * This information is cached.
 **/
void
gwy_scatter_xrange(GwyScatter *scatter,
                   gdouble *min,
                   gdouble *max)
{
    g_return_if_fail(GWY_IS_SCATTER(scatter));
    ensure_ranges(scatter);
    if (min)
        *min = scatter->priv->xymin.x;
    if (max)
        *max = scatter->priv->xymax.x;
}

/**
 * gwy_scatter_yrange:
 * @scatter: A scattered data.
 * @min: (out): Location where to store the minimum value.
 * @max: (out): Location where to store the maximum value.
 *
 * Gets the range of Y coordinates of an XYZ scattered data.
 *
 * This information is cached.
 **/
void
gwy_scatter_yrange(GwyScatter *scatter,
                   gdouble *min,
                   gdouble *max)
{
    g_return_if_fail(GWY_IS_SCATTER(scatter));
    ensure_ranges(scatter);
    if (min)
        *min = scatter->priv->xymin.y;
    if (max)
        *max = scatter->priv->xymax.y;
}

/**
 * gwy_scatter_min_max:
 * @scatter: A scattered data.
 * @i: Index of a value in @scatter.
 * @min: (out): Location where to store the minimum value.
 * @max: (out): Location where to store the maximum value.
 *
 * Gets the range of one value in a scattered data.
 *
 * This information is cached.
 **/
void
gwy_scatter_min_max(GwyScatter *scatter,
                    gint i,
                    gdouble *min,
                    gdouble *max)
{
    g_return_if_fail(GWY_IS_SCATTER(scatter));
    GwyScatterPrivate *priv = scatter->priv;
    g_return_if_fail(i >= 0 && i < priv->nvalues);
    ensure_ranges(scatter);
    if (min)
        *min = priv->valuemin[i];
    if (max)
        *max = priv->valuemax[i];
}

/**
 * gwy_scatter_multiply:
 * @scatter: A scattered data.
 * @i: Index of a value in @scatter.
 * @value: The value to multiply all data with.
 *
 * Multiplies values in a scattered data by given constant.
 **/
void
gwy_scatter_multiply(GwyScatter *scatter,
                     gint i,
                     gdouble value)
{
    g_return_if_fail(GWY_IS_SCATTER(scatter));
    GwyScatterPrivate *priv = scatter->priv;
    g_return_if_fail(i >= 0 && i < priv->nvalues);

    gsize n = scatter->n;
    gdouble *data = priv->data + i*n;
    for (gsize k = 0; k < n; k++)
        data[k] *= value;

    gwy_scatter_invalidate(scatter);
}

/**
 * gwy_scatter_add:
 * @scatter: A scattered data.
 * @i: Index of a value in @scatter.
 * @value: The value to add to all data.
 *
 * Adds a constant to values in a scattered data.
 **/
void
gwy_scatter_add(GwyScatter *scatter,
                gint i,
                gdouble value)
{
    g_return_if_fail(GWY_IS_SCATTER(scatter));
    GwyScatterPrivate *priv = scatter->priv;
    g_return_if_fail(i >= 0 && i < priv->nvalues);

    gsize n = scatter->n;
    gdouble *data = priv->data + i*n;
    for (gsize k = 0; k < n; k++)
        data[k] += value;

    gwy_scatter_invalidate(scatter);
}

/**
 * gwy_scatter_scale_xy:
 * @scatter: A scattered data.
 * @xfactor: Factor to multiply all X coordinates with.
 * @yfactor: Factor to multiply all Y coordinates with.
 * @xoffset: Offset to add to all X coordinates.
 * @yoffset: Offset to add to all Y coordinates.
 *
 * Rescales and/or shifts all X and Y coordinates of an XYZ scattered data.
 *
 * The multiplicative factors are applied before the offsets.
 **/
void
gwy_scatter_scale_xy(GwyScatter *scatter,
                     gdouble xfactor,
                     gdouble yfactor,
                     gdouble xoffset,
                     gdouble yoffset)
{
    g_return_if_fail(GWY_IS_SCATTER(scatter));

    gsize n = scatter->n;
    GwyXY *xy = scatter->priv->xy;
    for (gsize i = 0; i < n; i++) {
        xy[i].x = xfactor*xy[i].x + xoffset;
        xy[i].y = yfactor*xy[i].y + yoffset;
    }

    gwy_scatter_invalidate(scatter);
}

static void
ensure_ranges(GwyScatter *scatter)
{
    GwyScatterPrivate *priv = scatter->priv;
    if (priv->cached_ranges)
        return;

    gsize n = scatter->n;
    gint nvalues = priv->nvalues;
    priv->cached_ranges = TRUE;
    if (!n) {
        gwy_clear(&priv->xymin, 1);
        gwy_clear(&priv->xymax, 1);
        gwy_clear(priv->valuemin, nvalues);
        gwy_clear(priv->valuemax, nvalues);
        return;
    }

    const GwyXY *xy = priv->xy;
    gdouble xmin = G_MAXDOUBLE, ymin = G_MAXDOUBLE, xmax = -G_MAXDOUBLE, ymax = -G_MAXDOUBLE;
#ifdef _OPENMP
#pragma omp parallel for if(gwy_threads_are_enabled()) default(none) \
            reduction(min:xmin,ymin) reduction(max:xmax,ymax) \
            shared(xy,n)
#endif
    for (gsize i = 0; i < n; i++) {
        GwyXY pt = xy[i];

        xmin = fmin(xmin, pt.x);
        ymin = fmin(ymin, pt.y);
        xmax = fmax(xmax, pt.x);
        ymax = fmax(ymax, pt.y);
    }
    priv->xymin.x = xmin;
    priv->xymin.y = ymin;
    priv->xymax.x = xmax;
    priv->xymax.y = ymax;

    const gdouble *data = priv->data;
    for (gint j = 0; j < nvalues; j++) {
        gdouble vmin = G_MAXDOUBLE, vmax = -G_MAXDOUBLE;
        const gdouble *jdata = data + n*j;
#ifdef _OPENMP
#pragma omp parallel for if(gwy_threads_are_enabled()) default(none) \
            reduction(min:vmin) reduction(max:vmax) \
            shared(jdata,n)
#endif
        for (gsize i = 0; i < n; i++) {
            vmin = fmin(vmin, jdata[i]);
            vmax = fmax(vmax, jdata[i]);
        }
        priv->valuemin[j] = vmin;
        priv->valuemax[j] = vmax;
    }
}

/**
 * gwy_scatter_xy_is_compatible:
 * @scatter: A scattered data.
 * @otherscatter: Another scattered data.
 *
 * Checks whether the XY positions of two scattered data objects are compatible.
 *
 * Compatible XY positions mean the XY units are the same and the points are the same.  Two scatters that have the
 * same set of XY points but in different orders are <emphasis>not</emphasis> considered compatible.  This is because
 * the points at the same index in @data are different and thus calculations involving data from the two scatters are
 * impossible. It is necessary to match the points order in the two scatters to make this possible.
 *
 * This information is cached.
 *
 * Returns: %TRUE if the scatters are XY-position compatible, %FALSE if they are not.
 **/
// Keep this as a private subroutine to be called from a arithmetic.c general data mismatch function.
GwyDataMismatchFlags
_gwy_scatter_check_xy_incompatible(GwyScatter *scatter,
                                   GwyScatter *otherscatter)
{
    GwyDataMismatchFlags flag = GWY_DATA_MISMATCH_POINTS;
    g_return_val_if_fail(GWY_IS_SCATTER(scatter), flag);
    g_return_val_if_fail(GWY_IS_SCATTER(otherscatter), flag);

    GwyScatterPrivate *priv = scatter->priv, *opriv = otherscatter->priv;
    ensure_checksum(scatter);
    ensure_checksum(otherscatter);
    return memcmp(priv->checksum, opriv->checksum, sizeof(priv->checksum)) ? flag : 0;
}

static void
ensure_checksum(GwyScatter *scatter)
{
    GwyScatterPrivate *priv = scatter->priv;
    if (priv->cached_checksum)
        return;

    gsize n = scatter->n;
    GChecksum *checksummer = priv->checksummer;
    if (!checksummer)
        priv->checksummer = checksummer = g_checksum_new(G_CHECKSUM_MD5);
    else
        g_checksum_reset(checksummer);

    g_checksum_update(checksummer, (gchar*)priv->xy, n*sizeof(GwyXY));
    gsize len = G_N_ELEMENTS(priv->checksum);
    g_checksum_get_digest(checksummer, priv->checksum, &len);
    g_assert(len == G_N_ELEMENTS(priv->checksum));

    priv->cached_checksum = TRUE;
}

/**
 * gwy_scatter_resize:
 * @scatter: A scattered data.
 * @n: New number of points in the scattered data.
 *
 * Changes the number of points in a scattered data.
 *
 * If the number of points decreases then the first @npoints points will be kept. If the number of points increases
 * the new points will be uninitialised. Although typically one overwrites all the points after using this function.
 *
 * Use gwy_scatter_set_xy_full() if you already have an array of points to fill the scattered data with.
 **/
void
gwy_scatter_resize(GwyScatter *scatter, gsize n)
{
    g_return_if_fail(GWY_IS_SCATTER(scatter));
    if (n == scatter->n)
        return;

    GwyScatterPrivate *priv = scatter->priv;
    gint nvalues = priv->nvalues;
    priv->xy = g_renew(GwyXY, priv->xy, n);
    gdouble *newdata = g_new(gdouble, n*nvalues);
    if (n) {
        for (gint i = 0; i < nvalues; i++)
            gwy_assign(newdata + i*n, priv->data + i*scatter->n, n);
    }

    GWY_SWAP(gdouble*, priv->data, newdata);
    g_free(newdata);
    scatter->n = n;

    gwy_scatter_invalidate(scatter);
}

static void
reduce_points(GwyScatter *scatter, GwyXY *destxy, gdouble *destdata, gsize n)
{
    GwyScatterPrivate *priv = scatter->priv;
    GwyRandGenSet *rngset = gwy_rand_gen_set_new(1);
    GRand *rng = gwy_rand_gen_set_rng(rngset, 0);
    GwyXY *srcxy = priv->xy;
    gsize nfull = scatter->n;
    gint nvalues = priv->nvalues;

    // Dear compiler, if xlen and ylen are uninitialised, then xres=1 and yres=1 are also kept unchanged, and the
    // function returns early. Can't you see it?
    gdouble xmin, xmax, ymin, ymax, xlen = sizeof("GCC is dumb."), ylen = sizeof("GCC is dumb"), d;
    gint xres = 1, yres = 1, i, j;

    /* If a reasonable number of points is requested try to spread them. */
    if (n > 3) {
        gwy_scatter_xrange(scatter, &xmin, &xmax);
        gwy_scatter_yrange(scatter, &ymin, &ymax);
        xlen = xmax - xmin;
        ylen = ymax - ymin;
        gboolean x_is_degenerate = (xmin >= xmax);
        gboolean y_is_degenerate = (ymin >= ymax);
        if (x_is_degenerate && y_is_degenerate) {
            /* Choose points randomly */
            gwy_debug("both x and y are degenerate");
            d = 0.5*fmax(fabs(xmax), fabs(ymax));
            if (d == 0.0)
                d = 1.0;
        }
        if (x_is_degenerate && !y_is_degenerate) {
            gwy_debug("x is degenerate");
            /* Vertical line */
            xres = 1;
            yres = 3*n;
            d = 0.5*ylen/yres;
        }
        else if (y_is_degenerate && !x_is_degenerate) {
            gwy_debug("y is degenerate");
            /* Horizontal line */
            xres = 3*n;
            yres = 1;
            d = 0.5*xlen/xres;
        }
        else {
            gwy_debug("neither x nor y is degenerate");
            d = sqrt(4.0*xlen*ylen/n);
            if (xlen >= ylen) {
                yres = (gint)floor(ylen/d + 0.5);
                xres = (4*n + yres-1)/yres;
                gwy_debug("choosing using x");
            }
            else {
                xres = (gint)floor(xlen/d + 0.5);
                yres = (4*n + xres-1)/xres;
                gwy_debug("choosing using y");
            }
            d *= 0.5;
        }
        if (!xres || !yres)
            xres = yres = 1;
        xmin -= d;
        ymin -= d;
        xmax += d;
        ymax += d;
        xlen = xmax - xmin;
        ylen = ymax - ymin;
        gwy_debug("xres %d, yres %d", xres, yres);
    }

    const gdouble *srcdata = priv->data;
    if (xres == 1 && yres == 1) {
        // FIXME: This does not work if nfull does not fit into 32bit integer.
        guint *redindex = gwy_rand_gen_set_choose_shuffle(rngset, 0, nfull, n);
        for (gsize k = 0; k < n; k++) {
            gsize rik = redindex[k];
            destxy[k] = srcxy[rik];
            for (gint m = 0; m < nvalues; m++)
                destdata[m*n + k] = srcdata[m*nfull + rik];
        }
        g_free(redindex);
        gwy_rand_gen_set_free(rngset);
        return;
    }

    /* Count the number of points in each rectangle. */
    gint *grid = g_new0(gint, xres*yres);
    guint ngrid = 0;
    for (gsize k = 0; k < nfull; k++) {
        j = (gint)floor((srcxy[k].x - xmin)/xlen*xres);
        i = (gint)floor((srcxy[k].y - ymin)/ylen*yres);
        j = GWY_CLAMP(j, 0, xres-1);
        i = GWY_CLAMP(i, 0, yres-1);
        if (!grid[i*xres + j])
            ngrid++;
        grid[i*xres + j]++;
    }
    gwy_debug("points in rectangles %u", ngrid);

    /* Select one point in each rectangle by generating the index of point we should retain. */
    if (ngrid < n) {
        /* Use a point from each rectangle and then fill the rest randomly. */
        for (i = 0; i < xres*yres; i++) {
            if (grid[i] > 1)
                grid[i] = g_rand_int_range(rng, 1, grid[i]+1);
        }
        /* Find the indices of selected points. */
        gsize *pts = g_new0(gsize, nfull);
        for (gsize k = ngrid = 0; k < nfull; k++) {
            j = (gint)floor((srcxy[k].x - xmin)/xlen*xres);
            i = (gint)floor((srcxy[k].y - ymin)/ylen*yres);
            j = GWY_CLAMP(j, 0, xres-1);
            i = GWY_CLAMP(i, 0, yres-1);
            if (grid[i*xres + j] > 1)
                grid[i*xres + j]--;
            else if (grid[i*xres + j] == 1) {
                destxy[ngrid] = srcxy[k];
                for (gint m = 0; m < nvalues; m++)
                    destdata[m*n + ngrid] = srcdata[m*nfull + k];
                pts[k] = TRUE;
                grid[i*xres + j] = 0;
                ngrid++;
            }
        }

        /* Start filling the new scatter with points from rectangles. */
        gwy_debug("filled from grid %u", ngrid);

        /* Fill the rest with randomly selected points. */
        for (i = j = 0; i < nfull; i++) {
            if (!pts[i])
                pts[j++] = i;
        }
        // FIXME: This does not work if nfull does not fit into 32bit integer.
        guint *redindex = gwy_rand_gen_set_choose_shuffle(rngset, 0, nfull - ngrid, n - ngrid);
        for (gsize k = 0; k < (gsize)(n - ngrid); k++) {
            gsize rik = redindex[k];
            destxy[ngrid + k] = srcxy[rik];
            for (gint m = 0; m < nvalues; m++)
                destdata[m*n + ngrid + k] = srcdata[m*nfull + rik];
        }

        g_free(pts);
        g_free(redindex);
        gwy_debug("filled randomly %u", (n - ngrid));
    }
    else {
        if (ngrid > n) {
            /* We have more points in rectangles than requested so remove some of them randomly. */
            gwy_debug("removing %u rectangles", ngrid - n);
            gsize *pts = g_new0(gsize, ngrid);
            for (i = j = 0; i < xres*yres; i++) {
                if (grid[i])
                    pts[j++] = i;
            }
            // FIXME: This does not work if ngrid does not fit into 32bit integer.
            guint *redindex = gwy_rand_gen_set_choose_shuffle(rngset, 0, ngrid, ngrid - n);
            for (i = 0; i < ngrid - n; i++)
                grid[pts[redindex[i]]] = 0;
            g_free(redindex);
            g_free(pts);
        }

        /* Use a point from each remaining rectangle. */
        for (i = 0; i < xres*yres; i++) {
            if (grid[i] > 1)
                grid[i] = g_rand_int_range(rng, 1, grid[i]+1);
        }
        ngrid = 0;
        for (gsize k = 0; k < nfull; k++) {
            j = (gint)floor((srcxy[k].x - xmin)/xlen*xres);
            i = (gint)floor((srcxy[k].y - ymin)/ylen*yres);
            j = GWY_CLAMP(j, 0, xres-1);
            i = GWY_CLAMP(i, 0, yres-1);
            if (grid[i*xres + j] > 1)
                grid[i*xres + j]--;
            else if (grid[i*xres + j] == 1) {
                destxy[ngrid] = srcxy[k];
                for (gint m = 0; m < nvalues; m++)
                    destdata[m*n + ngrid] = srcdata[m*nfull + k];
                grid[i*xres + j] = 0;
                ngrid++;
            }
        }
    }
    g_free(grid);
    gwy_rand_gen_set_free(rngset);
}

/**
 * gwy_scatter_new_reduced_points:
 * @scatter: A scattered data.
 * @n: Requested number of points in the reduced scattered data.  If it is not smaller than the number of points in
 *     @scatter then the function behaves like gwy_scatter_copy().
 *
 * Creates a similar scattered data with smaller number of points.
 *
 * The functions attempts to choose points from the original scattered data to cover its full area, even though points
 * from dense regions are still more likely to be found in the result than points from sparse regions.  As the main
 * purpose to enable quick rough operations that may take a long time with the full scattered data, the focus is on
 * speed not fidelity.
 *
 * The function may employ random selection and thus be be non-deterministic.
 *
 * Returns: (transfer full): A newly created #GwyScatter with reduced number of points.
 **/
GwyScatter*
gwy_scatter_new_reduced_points(GwyScatter *scatter, gsize n)
{
    g_return_val_if_fail(GWY_IS_SCATTER(scatter), NULL);
    gsize nfull = scatter->n;
    if (n >= nfull)
        return gwy_scatter_copy(scatter);
    if (!n)
        return gwy_scatter_new_alike(scatter);

    GwyScatter *reduced = gwy_scatter_new_alike(scatter);
    GwyScatterPrivate *priv = reduced->priv;
    gint nvalues = priv->nvalues;
    alloc_data(reduced, n, nvalues);
    reduce_points(scatter, priv->xy, priv->data, n);

    return reduced;
}

/**
 * gwy_scatter_reduce_points:
 * @scatter: A scattered data.
 * @n: Requested number of points in the reduced scattered data.  If it is not smaller than the number of points in
 *     @scatter the number of points is unchanged.
 *
 * Reduces the number of points in scattered data.
 *
 * The functions attempts to choose points from the original scattered data to cover its full area, even though points
 * from dense regions are still more likely to be found in the result than points from sparse regions.  As the main
 * purpose to enable quick rough operations that may take a long time with the full scattered data, the focus is on
 * speed not fidelity.
 *
 * The function may employ random selection and thus be be non-deterministic.
 *
 * If you take advantage of the possibility of passing @n larger than the current number of points, you cannot assume
 * that after the reduction the scattered data will have @n points (if the number is actually reduced, then you can).
 **/
void
gwy_scatter_reduce_points(GwyScatter *scatter, gsize n)
{
    g_return_if_fail(GWY_IS_SCATTER(scatter));
    gsize nfull = scatter->n;
    if (n >= nfull)
        return;

    GwyScatterPrivate *priv = scatter->priv;
    gint nvalues = priv->nvalues;
    if (!n) {
        alloc_data(scatter, 0, nvalues);
        gwy_scatter_invalidate(scatter);
        return;
    }

    GwyXY *rxy = g_new(GwyXY, n);
    gdouble *rdata = g_new(gdouble, n*nvalues);
    reduce_points(scatter, rxy, rdata, n);
    GWY_SWAP(GwyXY*, priv->xy, rxy);
    GWY_SWAP(gdouble*, priv->data, rdata);
    scatter->n = n;
    g_free(rxy);
    g_free(rdata);
    gwy_scatter_invalidate(scatter);
}

static guint32*
filter_func_to_keep_mask(GwyScatter *scatter,
                         GwyScatterFilterFunc filter,
                         gpointer user_data,
                         gsize *pnkeep)
{
    GwyScatterPrivate *priv = scatter->priv;
    gsize n = scatter->n;
    gint nvalues = priv->nvalues;
    if (!n) {
        *pnkeep = 0;
        return NULL;
    }

    GwyXY *xy = priv->xy;
    gdouble *data = priv->data;
    guint32 *keep = g_new0(guint32, (n + 31)/32);
    gdouble *values = g_new(gdouble, nvalues);
    gsize nkeep = 0;
    for (gsize k = 0; k < n; k++) {
        for (gint m = 0; m < nvalues; m++)
            values[m] = data[m*n + k];
        if (filter(xy + k, values, user_data)) {
            keep[k/32] |= (1u << (k % 32));
            nkeep++;
        }
    }
    g_free(values);

    *pnkeep = nkeep;

    return keep;
}

/**
 * gwy_scatter_filter:
 * @scatter: A scattered data.
 * @filter: (scope call) (closure user_data):
 *          Function returning %TRUE for points to keep and %FALSE for points to discard.
 * @user_data: User data passed to @filter.
 *
 * Filters scattered data using a user-specified filter function.
 *
 * See also gwy_scatter_reduce_to_subset() which filters scattered data using a bit mask.
 *
 * Returns: The new number of points in @scatter.
 **/
gsize
gwy_scatter_filter(GwyScatter *scatter,
                   GwyScatterFilterFunc filter,
                   gpointer user_data)
{
    g_return_val_if_fail(GWY_IS_SCATTER(scatter), 0);
    g_return_val_if_fail(filter, 0);

    GwyScatterPrivate *priv = scatter->priv;
    gsize nkeep, n = scatter->n;
    guint32 *keep;
    if (!(keep = filter_func_to_keep_mask(scatter, filter, user_data, &nkeep))) {
        alloc_data(scatter, 0, priv->nvalues);
        gwy_scatter_invalidate(scatter);
        return 0;
    }
    if (nkeep == n) {
        g_free(keep);
        return n;
    }

    GwyXY *newxy = g_new(GwyXY, nkeep);
    gdouble *newdata = g_new(gdouble, nkeep*priv->nvalues);
    filter_points(scatter, keep, newxy, newdata, nkeep);
    GWY_SWAP(GwyXY*, newxy, priv->xy);
    GWY_SWAP(gdouble*, newdata, priv->data);
    scatter->n = nkeep;
    g_free(keep);
    g_free(newxy);
    g_free(newdata);

    return nkeep;
}

/**
 * gwy_scatter_new_filtered:
 * @scatter: A scattered data.
 * @filter: (scope call) (closure user_data):
 *          Function returning %TRUE for points to keep and %FALSE for points to discard.
 * @user_data: User data passed to @filter.
 *
 * Creates new scattered data by filtering another data using a user-specified filter function.
 *
 * See also gwy_scatter_new_subset() which filters scattered data using a bit mask.
 *
 * Returns: (transfer full): Newly created filtered scattered data.
 **/
GwyScatter*
gwy_scatter_new_filtered(GwyScatter *scatter,
                         GwyScatterFilterFunc filter,
                         gpointer user_data)
{
    g_return_val_if_fail(GWY_IS_SCATTER(scatter), NULL);
    g_return_val_if_fail(filter, NULL);

    GwyScatterPrivate *priv = scatter->priv;
    gsize nkeep, n = scatter->n;
    guint32 *keep;
    if (!(keep = filter_func_to_keep_mask(scatter, filter, user_data, &nkeep)))
        return gwy_scatter_new_alike(scatter);
    if (nkeep == n) {
        g_free(keep);
        return gwy_scatter_copy(scatter);
    }

    GwyScatter *filtered = gwy_scatter_new_alike(scatter);
    alloc_data(filtered, nkeep, priv->nvalues);
    GwyXY *fxy = filtered->priv->xy;
    gdouble *fdata = filtered->priv->data;
    filter_points(scatter, keep, fxy, fdata, nkeep);
    g_free(keep);

    return filtered;
}

#if (defined(__GNUC__) || defined(__clang__))
#define count_set_bits __builtin_popcount
#else
static inline guint
count_set_bits(guint32 x)
{
    static const guint16 table[256] = {
        0, 1, 1, 2, 1, 2, 2, 3, 1, 2, 2, 3, 2, 3, 3, 4,
        1, 2, 2, 3, 2, 3, 3, 4, 2, 3, 3, 4, 3, 4, 4, 5,
        1, 2, 2, 3, 2, 3, 3, 4, 2, 3, 3, 4, 3, 4, 4, 5,
        2, 3, 3, 4, 3, 4, 4, 5, 3, 4, 4, 5, 4, 5, 5, 6,
        1, 2, 2, 3, 2, 3, 3, 4, 2, 3, 3, 4, 3, 4, 4, 5,
        2, 3, 3, 4, 3, 4, 4, 5, 3, 4, 4, 5, 4, 5, 5, 6,
        2, 3, 3, 4, 3, 4, 4, 5, 3, 4, 4, 5, 4, 5, 5, 6,
        3, 4, 4, 5, 4, 5, 5, 6, 4, 5, 5, 6, 5, 6, 6, 7,
        1, 2, 2, 3, 2, 3, 3, 4, 2, 3, 3, 4, 3, 4, 4, 5,
        2, 3, 3, 4, 3, 4, 4, 5, 3, 4, 4, 5, 4, 5, 5, 6,
        2, 3, 3, 4, 3, 4, 4, 5, 3, 4, 4, 5, 4, 5, 5, 6,
        3, 4, 4, 5, 4, 5, 5, 6, 4, 5, 5, 6, 5, 6, 6, 7,
        2, 3, 3, 4, 3, 4, 4, 5, 3, 4, 4, 5, 4, 5, 5, 6,
        3, 4, 4, 5, 4, 5, 5, 6, 4, 5, 5, 6, 5, 6, 6, 7,
        3, 4, 4, 5, 4, 5, 5, 6, 4, 5, 5, 6, 5, 6, 6, 7,
        4, 5, 5, 6, 5, 6, 6, 7, 5, 6, 6, 7, 6, 7, 7, 8,
    };
    guint count = 0;
    count += table[x & 0xff];
    x >>= 8;
    count += table[x & 0xff];
    x >>= 8;
    count += table[x & 0xff];
    x >>= 8;
    count += table[x & 0xff];
    return count;
}
#endif

static gsize
count_set_bits_array(const guint32 *array, gsize n)
{
    gsize count = 0;
    for (gsize k = 0; k < n/32; k++)
        count += count_set_bits(array[k]);
    // Be careful with the last block if the number of points is not a multiple of 32. Do not count bits corresponding
    // to values outside the data.
    guint rem = n % 32;
    if (rem) {
        guint32 bits = array[n/32];
        guint32 mask = G_MAXUINT32 >> (32 - rem);
        count += count_set_bits(bits & mask);
    }
    return count;
}

/**
 * gwy_scatter_new_part:
 * @scatter: A scattered data.
 * @xfrom: Minimum x-coordinate value.
 * @xto: Maximum x-coordinate value.
 * @yfrom: Minimum y-coordinate value.
 * @yto: Maximum y-coordinate value.
 *
 * Creates a new scattered data as a part of another scattered data.
 *
 * The new scattered data consits of data with lateral coordinates within the specified ranges (inclusively).  It may
 * be empty.
 *
 * Data are physically copied, i.e. changing the new scattered data does not change @scatter's data and vice versa.
 *
 * Returns: (transfer full):
 *          A newly created scattered data.
 **/
GwyScatter*
gwy_scatter_new_part(GwyScatter *scatter,
                     gdouble xfrom,
                     gdouble xto,
                     gdouble yfrom,
                     gdouble yto)
{
    g_return_val_if_fail(GWY_IS_SCATTER(scatter), NULL);

    GwyScatter *part = gwy_scatter_new_alike(scatter);
    if (!scatter->n || xfrom > xto || yfrom > yto)
        return part;

    GwyScatterPrivate *priv = scatter->priv, *ppriv = part->priv;
    const GwyXY *xy = priv->xy;
    gsize n = 0, nfull = scatter->n;
    guint32 *keep = g_new0(guint32, (nfull + 31)/32);
    for (gsize i = 0; i < nfull; i++) {
        gdouble x = xy[i].x, y = xy[i].y;
        if (x >= xfrom && x <= xto && y >= yfrom && y <= yto) {
            keep[i/32] |= (1u << (i % 32));
            n++;
        }
    }

    gint nvalues = priv->nvalues;
    alloc_data(part, n, nvalues);
    filter_points(scatter, keep, ppriv->xy, ppriv->data, n);
    g_free(keep);

    return part;
}

/**
 * gwy_scatter_reduce_to_subset:
 * @scatter: A scattered data.
 * @keepbits: Bit mask defining which data to keep.
 *
 * Keeps a subset of scattered data points, defined using a bitmask.
 *
 * The @scatter array must have at least ceil(gwy_scatter_get_n_points()/32) elements. Bits in the array correspond
 * to the scattered data – in order. Within each 32bit integer, the bits go from the lowest to the highest.
 *
 * See also gwy_scatter_filter() which filters scattered data using a filter function.
 *
 * Returns: The number of values in the reduced data (given by the number of set bits in @keepbits).
 **/
gsize
gwy_scatter_reduce_to_subset(GwyScatter *scatter,
                             const guint32 *keepbits)
{
    g_return_val_if_fail(scatter, 0);
    if (!keepbits) {
        gwy_scatter_resize(scatter, 0);
        return 0;
    }

    gsize n = scatter->n, nkeep = count_set_bits_array(keepbits, n);
    GwyScatterPrivate *priv = scatter->priv;
    gint nvalues = priv->nvalues;
    if (nkeep == n || !nkeep) {
        if (!nkeep)
            alloc_data(scatter, 0, nvalues);
    }
    else {
        GwyXY *newxy = g_new(GwyXY, nkeep);
        gdouble *newdata = g_new(gdouble, nkeep*nvalues);
        filter_points(scatter, keepbits, newxy, newdata, nkeep);
        GWY_SWAP(GwyXY*, newxy, priv->xy);
        GWY_SWAP(gdouble*, newdata, priv->data);
        scatter->n = nkeep;
    }
    if (nkeep != n)
        gwy_scatter_invalidate(scatter);

    return nkeep;
}

/**
 * gwy_scatter_new_subset:
 * @scatter: A scattered data.
 * @keepbits: Bit mask defining which data to keep.
 *
 * Creates new scattered data as a subset of another, defined using a bitmask.
 *
 * The @scatter array must have at least ceil(gwy_scatter_get_n_points()/32) elements. Bits in the array correspond
 * to the scattered data – in order. Within each 32bit integer, the bits go from the lowest to the highest.
 *
 * See also gwy_scatter_new_filtered() which filters scattered data using a filter function.
 *
 * Returns: (transfer full): Newly created filtered scattered data.
 **/
GwyScatter*
gwy_scatter_new_subset(GwyScatter *scatter,
                       const guint32 *keepbits)
{
    g_return_val_if_fail(scatter, NULL);
    if (!keepbits)
        return gwy_scatter_new_alike(scatter);

    gsize n = scatter->n, nkeep = count_set_bits_array(keepbits, n);
    GwyScatterPrivate *priv = scatter->priv;
    if (!nkeep)
        return gwy_scatter_new_alike(scatter);
    if (nkeep == n)
        return gwy_scatter_copy(scatter);

    GwyScatter *filtered = gwy_scatter_new_alike(scatter);
    alloc_data(filtered, nkeep, priv->nvalues);
    GwyXY *fxy = filtered->priv->xy;
    gdouble *fdata = filtered->priv->data;
    filter_points(scatter, keepbits, fxy, fdata, nkeep);

    return filtered;
}

static void
filter_points(GwyScatter *scatter,
              const guint32 *keep,
              GwyXY *outxy,
              gdouble *outdata,
              gsize nout)
{
    GwyScatterPrivate *priv = scatter->priv;
    gsize n = scatter->n;
    gint nvalues = priv->nvalues;
    const GwyXY *xy = priv->xy;
    const gdouble *data = priv->data;

    if (!n)
        return;

    // Copy data by contiguous chunks in the hope the pattern is not like altenating keep/discard and we will be able
    // to do most of the work by a few big memcpy()s.
    gsize k = 0, kout = 0, chunklen = 0, k_for_incomplete_block = (n/32)*32;
    while (k < n) {
        guint32 bits = keep[k/32];
        if (k == k_for_incomplete_block) {
            // Clear bits corresponding to values outside the data so that we never try to copy the corresponding
            // data. Also that the cycle terminates immediately when we get to the end of data.
            guint rem = n % 32;
            bits &= (G_MAXUINT32 >> (32 - rem));
        }
        guint kk;
        for (kk = 0; bits; kk++) {
            if (bits & 1)
                chunklen++;
            else if (chunklen) {
                g_return_if_fail(kout + chunklen <= nout);
                gsize srcpos = k - chunklen;
                gwy_assign(outxy + kout, xy + srcpos, chunklen);
                for (gint j = 0; j < nvalues; j++)
                    gwy_assign(outdata + j*nout + kout, data + j*n + srcpos, chunklen);
                kout += chunklen;
                chunklen = 0;
            }
            bits >>= 1u;
            k++;
        }

        // Skip the remainder of the 32-point block (which can be even all 32 points). Keep in mind that when we skip
        // anything there can be keep-chunk to copy.
        if (kk < 32 || k == n) {
            if (chunklen) {
                g_return_if_fail(kout + chunklen <= nout);
                gsize srcpos = k - chunklen;
                gwy_assign(outxy + kout, xy + srcpos, chunklen);
                for (gint j = 0; j < nvalues; j++)
                    gwy_assign(outdata + j*nout + kout, data + j*n + srcpos, chunklen);
                kout += chunklen;
                chunklen = 0;
            }
            // This move to a 32-point block boundary can overshoot the output array. Which is OK as long as we do not
            // use k to access anything afterwards.
            k += 32 - kk;
        }
    }
}

/**
 * gwy_scatter_sort:
 * @scatter: A scattered data.
 * @compare: (scope call) (closure user_data):
 *           Point comparison function.
 * @user_data: User data passed to @compare.
 *
 * Sorts scattered data using a user-provided function.
 *
 * See also gwy_scatter_sort_by_value() if you want to simple sort by one data value, for example time.
 **/
void
gwy_scatter_sort(GwyScatter *scatter,
                 GwyScatterCompareFunc compare,
                 gpointer user_data)
{
    g_return_if_fail(GWY_IS_SCATTER(scatter));
    g_return_if_fail(compare);

    GwyScatterPrivate *priv = scatter->priv;
    gsize n = scatter->n;
    gdouble *packed = gwy_scatter_pack(scatter, -1);
#if GLIB_CHECK_VERSION(2,82,0)
    g_sort_array(packed, n, (priv->nvalues+2)*sizeof(gdouble), (GCompareDataFunc)compare, user_data);
#else
    g_qsort_with_data(packed, n, (priv->nvalues+2)*sizeof(gdouble), (GCompareDataFunc)compare, user_data);
#endif
    gwy_scatter_set_from_packed(scatter, packed, priv->nvalues, n);
    g_free(packed);
}

/**
 * gwy_scatter_sort_by_value:
 * @scatter: A scattered data.
 * @i: Index of data value to use for comparison.
 * @ascending: %TRUE to sort in ascending order, %FALSE in descending order.
 *
 * Sorts scattered data using one data value.
 *
 * See also gwy_scatter_sort() for a general sorting function.
 **/
void
gwy_scatter_sort_by_value(GwyScatter *scatter,
                          gint i,
                          gboolean ascending)
{
    g_return_if_fail(GWY_IS_SCATTER(scatter));
    GwyScatterPrivate *priv = scatter->priv;
    g_return_if_fail(i >= 0 && i < priv->nvalues);

    // Sort the i-th value and create an index (permutation). Sort in the ascending order because we have a function
    // for it and reverse the order later if needed.
    gsize n = scatter->n;
    gdouble *data = priv->data;
    guint *index_array = g_new(guint, n);
    for (gsize k = 0; k < n; k++)
        index_array[k] = k;
    gwy_math_sort_with_index(data + i*n, n, index_array);

    // Apply the permutation to all other data.
    gint nvalues = priv->nvalues;
    gpointer storage = g_new(gdouble, 2*n);
    for (gint j = 0; j < nvalues; j++) {
        gdouble *dataj = data + j*n;
        if (j == i) {
            if (!ascending) {
                for (gsize k = 0; k < n/2; k++)
                    GWY_SWAP(gdouble, dataj[k], dataj[n-1 - k]);
            }
            continue;
        }

        gdouble *databuf = storage;
        if (ascending) {
            for (gsize k = 0; k < n; k++)
                databuf[k] = dataj[index_array[k]];
        }
        else {
            for (gsize k = 0; k < n; k++)
                databuf[n-1 - k] = dataj[index_array[k]];
        }
        gwy_assign(dataj, databuf, n);
    }

    // Apply the permutation to coordinates.
    GwyXY *xy = priv->xy, *xybuf = storage;
    if (ascending) {
        for (gsize k = 0; k < n; k++)
            xybuf[k] = xy[index_array[k]];
    }
    else {
        for (gsize k = 0; k < n; k++)
            xybuf[n-1 - k] = xy[index_array[k]];
    }
    gwy_assign(xy, xybuf, n);

    g_free(index_array);
    g_free(storage);
}

/**
 * gwy_scatter_stack:
 * @scatter: A scattered data.
 * @operands: (array size=n) (transfer none): Array with more compatible scattered data.
 * @n: Number of items in @operands.
 *
 * Adds data from scattered data as new values to another scattered data, ‘stacking’ the data.
 *
 * All the operands must share the set of XY points with @scatter. Meaning, they must pass %GWY_DATA_MISMATCH_POINTS
 * and %GWY_DATA_MISMATCH_LATERAL checks with gwy_scatter_is_incompatible(). Nevertheless, it is an error to try to
 * stack a scattered data to itself.
 *
 * All data from the operands are then added to @scatter, in given order, including units and labels. First all data
 * from the first operand are added, then all data from the second operand, etc.
 **/
void
gwy_scatter_stack(GwyScatter *scatter,
                  GwyScatter **operands,
                  guint n)
{
    g_return_if_fail(GWY_IS_SCATTER(scatter));
    g_return_if_fail(!n || operands);
    if (!n)
        return;

    GwyScatterPrivate *priv = scatter->priv;
    gboolean have_labels = !!priv->datalabels, need_labels = have_labels;
    GwyDataMismatchFlags flags = GWY_DATA_MISMATCH_LATERAL | GWY_DATA_MISMATCH_POINTS;
    gint total_nvalues = priv->nvalues;
    for (guint i = 0; i < n; i++) {
        GwyScatter *operand = operands[i];
        g_return_if_fail(GWY_IS_SCATTER(operand));
        g_return_if_fail(operand != scatter);
        g_return_if_fail(!gwy_scatter_is_incompatible(scatter, operand, flags));
        if (!need_labels)
            need_labels = !!operand->priv->datalabels;
        total_nvalues += operand->priv->nvalues;
    }
    if (total_nvalues == priv->nvalues)
        return;

    // This is one of the rare cases when we want to preserve the contents of values array. Steal it.
    gdouble *origdata = priv->data;
    gint orig_nvalues = priv->nvalues;
    priv->data = NULL;

    // Force allocation of labels when at least some operands have them but the target does not. Note that we cannot
    // simply allocate it with orig_nvalues elements. If orig_nvalues is zero, this will give a NULL pointer and
    // alloc_data() just keeps it NULL then.
    if (need_labels && !have_labels) {
        priv->datalabels = g_new0(gchar*, MAX(orig_nvalues, 1));
        set_default_data_labels(priv->datalabels, 0, orig_nvalues);
    }

    gsize npoints = scatter->n;
    alloc_data(scatter, npoints, total_nvalues);
    gwy_assign(priv->data, origdata, orig_nvalues*npoints);
    g_free(origdata);

    total_nvalues = orig_nvalues;
    for (guint i = 0; i < n; i++) {
        GwyScatter *operand = operands[i];
        GwyScatterPrivate *opriv = operand->priv;
        guint onvalues = opriv->nvalues;
        gwy_assign(priv->data + total_nvalues*npoints, opriv->data, onvalues*npoints);
        for (guint j = 0; j < onvalues; j++) {
            _gwy_copy_unit(opriv->units_data[j], priv->units_data + total_nvalues + j);
            if (opriv->datalabels)
                gwy_assign_string(priv->datalabels + total_nvalues + j, opriv->datalabels[j]);
        }
        if (need_labels && !opriv->datalabels)
            set_default_data_labels(priv->datalabels, total_nvalues, onvalues);
        total_nvalues += onvalues;
    }
    priv->nvalues = total_nvalues;

    gwy_scatter_invalidate(scatter);
}

/**
 * gwy_scatter_insert_values:
 * @scatter: A scattered data.
 * @i: Index in scattered data values where to insert the new values. It can be between zero and the current number
 *     of values (inclusive). The latter case means appending to the end.
 * @nvalues: The number of values to add.
 *
 * Increases the number of values per point in scattered data.
 *
 * The new data start as uninitialised. Use gwy_scatter_get_data() afterwards to get the data arrays and fill them.
 *
 * The XY coordinates remain unchanged.
 **/
void
gwy_scatter_insert_values(GwyScatter *scatter,
                          gint i,
                          gint nvalues)
{
    g_return_if_fail(GWY_IS_SCATTER(scatter));
    GwyScatterPrivate *priv = scatter->priv;
    g_return_if_fail(i >= 0 && i <= priv->nvalues);
    g_return_if_fail(nvalues >= 0);
    if (!nvalues)
        return;

    gsize n = scatter->n;
    gint oldnvalues = priv->nvalues, newnvalues = oldnvalues + nvalues, ntail = oldnvalues - i;
    gdouble *olddata = priv->data;
    gdouble *data = priv->data = g_new(gdouble, n*newnvalues);
    gwy_assign(data, olddata, i*n);
    // The middle data block is left uninitialised.
    gwy_assign(data + (i + nvalues)*n, olddata + i*n, ntail*n);
    g_free(olddata);

    priv->units_data = g_renew(GwyUnit*, priv->units_data, newnvalues);
    memmove(priv->units_data + i + nvalues, priv->units_data + i, ntail*sizeof(GwyUnit*));
    gwy_clear(priv->units_data + i, nvalues);

    if (priv->datalabels) {
        priv->datalabels = g_renew(gchar*, priv->datalabels, newnvalues);
        memmove(priv->datalabels + i + nvalues, priv->datalabels + i, ntail*sizeof(gchar*));
        set_default_data_labels(priv->datalabels, i, nvalues);
    }
    priv->nvalues = newnvalues;

    gwy_scatter_invalidate(scatter);
}

/**
 * gwy_scatter_remove_values:
 * @scatter: A scattered data.
 * @i: Index of the first scattered data value to remove. It can be between zero and the current number of values
 *     (inclusive). However, the latter case means @nvalues must be zero and is not particularly useful.
 * @nvalues: The number of values to remove. It must not exceed the number of values that exist in @scatter (starting
 *           from @i).
 *
 * Decreases the number of values per point in scattered data.
 *
 * The XY coordinates remain unchanged.
 **/
void
gwy_scatter_remove_values(GwyScatter *scatter,
                          gint i,
                          gint nvalues)
{
    g_return_if_fail(GWY_IS_SCATTER(scatter));
    GwyScatterPrivate *priv = scatter->priv;
    g_return_if_fail(i >= 0 && i <= priv->nvalues);
    g_return_if_fail(nvalues >= 0);
    gint oldnvalues = priv->nvalues;
    g_return_if_fail(i + nvalues <= oldnvalues);
    if (!nvalues)
        return;

    gsize n = scatter->n;
    gint newnvalues = oldnvalues - nvalues, ntail = oldnvalues - (i + nvalues);
    gboolean have_labels = !!priv->datalabels;
    // The head block is already in place. Move the tail block.
    memmove(priv->data + i*n, priv->data + (i + nvalues)*n, ntail*n*sizeof(gdouble));
    priv->data = g_renew(gdouble, priv->data, n*newnvalues);

    for (gint j = i; j < i + nvalues; j++)
        g_clear_object(priv->units_data + j);
    memmove(priv->units_data + i, priv->units_data + i + nvalues, ntail*sizeof(GwyUnit*));
    priv->units_data = g_renew(GwyUnit*, priv->units_data, newnvalues);

    if (priv->datalabels) {
        for (gint j = i; j < i + nvalues; j++)
            g_free(priv->datalabels[j]);
        memmove(priv->datalabels + i, priv->datalabels + i + nvalues, ntail*sizeof(gchar*));
    }
    else if (have_labels) {
        // Preserve the state of having labels even when the number of values drops to zero and priv->datalabels
        // becomes NULL (g_renew() behaves like glibc, i.e. realloc(ptr, 0) ≡ free(ptr), NULL).
        g_assert(newnvalues == 0);
        priv->datalabels = g_new0(gchar*, 1);
    }
    priv->nvalues = newnvalues;

    gwy_scatter_invalidate(scatter);
}

static void
serializable_itemize(GwySerializable *serializable, GwySerializableGroup *group)
{
    GwyScatter *scatter = GWY_SCATTER(serializable);
    GwyScatterPrivate *priv = scatter->priv;
    gint nvalues = priv->nvalues;

    gwy_serializable_group_alloc_size(group, NUM_ITEMS);
    gwy_serializable_group_append_int32(group, serializable_items + ITEM_NVALUES, priv->nvalues);
    gwy_serializable_group_append_unit(group, serializable_items + ITEM_UNIT_XY, priv->unit_xy);
    gwy_serializable_group_append_double_array(group, serializable_items + ITEM_XY,
                                               (gdouble*)priv->xy, 2*scatter->n);
    gwy_serializable_group_append_double_array(group, serializable_items + ITEM_DATA,
                                               priv->data, nvalues*scatter->n);

    guint existing_units = 0;
    for (guint i = 0; i < nvalues; i++)
        existing_units += (priv->units_data[i] && !gwy_unit_equal_string(priv->units_data[i], NULL));

    if (existing_units) {
        for (guint i = 0; i < nvalues; i++) {
            if (!priv->units_data[i])
                priv->units_data[i] = gwy_unit_new(NULL);
        }
        gwy_serializable_group_append_object_array(group, serializable_items + ITEM_UNITS_DATA,
                                                   (GObject**)priv->units_data, nvalues);
    }

    gwy_serializable_group_append_string_array(group, serializable_items + ITEM_DATA_LABELS,
                                               priv->datalabels, nvalues);

    gwy_serializable_group_itemize(group);
}

static gboolean
is_too_much(GwyErrorList **error_list, const gchar *name, guint value, guint max_value)
{
    if (G_LIKELY(value <= max_value))
        return FALSE;

    // Avoid translatable message proliferation by using %s format.
    gchar *s = g_strdup_printf("%u", value);
    gwy_error_list_add(error_list, GWY_DESERIALIZE_ERROR, GWY_DESERIALIZE_ERROR_INVALID,
                       _("Dimension %s of ‘%s’ is invalid."),
                       s, name);
    g_free(s);
    return TRUE;
}

static void
add_size_mismatch_error(GwyErrorList **error_list, const GwySerializableItem *it,
                        const gchar *expected_name, guint expected_n)
{
    /* NB: We do not say the numbers are not equal. Matching may not mean exactly equal. */
    gwy_error_list_add(error_list, GWY_DESERIALIZE_ERROR, GWY_DESERIALIZE_ERROR_REPLACED,
                       // TRANSLATORS: Error message.
                       // TRANSLATORS: Second %s is a data type name, e.g. GwyField.
                       _("The number of items in ‘%s’ of %s %" G_GSIZE_FORMAT " does not match "
                         "‘%s’ %u."),
                       it->name, TYPE_NAME, it->array_size, expected_name, expected_n);
}

static gboolean
serializable_construct(GwySerializable *serializable, GwySerializableGroup *group, GwyErrorList **error_list)
{
    GwySerializableItem its[NUM_ITEMS], *it;
    gboolean ok = FALSE;
    gwy_assign(its, serializable_items, NUM_ITEMS);
    gwy_deserialize_filter_items(its, NUM_ITEMS, group, TYPE_NAME, error_list);

    GwyScatter *scatter = GWY_SCATTER(serializable);
    GwyScatterPrivate *priv = scatter->priv;

    /* It is possibly to infer nvalues most of the time. A separate item is needed to preserve correctly data with no
     * points but nvalues > 1. */
    guint nvalues = its[ITEM_NVALUES].value.v_int32;
    /* Hundred times more than a reasonable large amount is too much. */
    if (is_too_much(error_list,TYPE_NAME ".nvalues", nvalues, 1024))
        goto fail;

    /* Botched up XY data dimensions is a hard fail. However, either npoints or nvalues can be zero. */
    it = its + ITEM_XY;
    gsize npoints = it->array_size/2;
    if (npoints) {
        gsize xy_dim[2] = { npoints, 2 };
        if (!gwy_check_data_dimension(error_list, TYPE_NAME ".xy", 2, it->array_size, xy_dim))
            goto fail;

        if (nvalues) {
            it = its + ITEM_DATA;
            gsize logical_dims[2] = { npoints, nvalues };
            if (!gwy_check_data_dimension(error_list, TYPE_NAME, 2, it->array_size, logical_dims))
                goto fail;
        }
    }
    /* Reject any data if there should not be any. */
    if (!npoints || !nvalues) {
        it = its + ITEM_DATA;
        if (it->array_size) {
            gchar *s = g_strdup_printf("%" G_GUINT64_FORMAT, it->array_size);
            gwy_error_list_add(error_list, GWY_DESERIALIZE_ERROR, GWY_DESERIALIZE_ERROR_INVALID,
                               _("Dimension %s of ‘%s’ is invalid."),
                               s, TYPE_NAME);
            g_free(s);
            goto fail;
        }
    }

    it = its + ITEM_XY;
    priv->xy = (GwyXY*)it->value.v_double_array;
    it->value.v_double_array = NULL;

    it = its + ITEM_DATA;
    priv->data = it->value.v_double_array;
    it->value.v_double_array = NULL;

    scatter->n = npoints;
    priv->nvalues = nvalues;

    /* The rest is already validated by pspec as far as GTypes and atomic types ranges go. */
    priv->unit_xy = (GwyUnit*)its[ITEM_UNIT_XY].value.v_object;
    its[ITEM_UNIT_XY].value.v_object = NULL;

    /* Constraint: len(datalabels) = nvalues. */
    it = its + ITEM_DATA_LABELS;
    if (it->array_size) {
        if (it->array_size == nvalues) {
            priv->datalabels = it->value.v_string_array;
            it->value.v_string_array = NULL;
        }
        else
            add_size_mismatch_error(error_list, it, "nvalues", nvalues);
    }
    else {
        GWY_FREE(priv->datalabels);
    }

    /* Constraint: len(dataunits) = nvalues. */
    it = its + ITEM_UNITS_DATA;
    if (it->array_size) {
        if (it->array_size == nvalues) {
            priv->units_data = (GwyUnit**)it->value.v_object_array;
            it->value.v_object_array = NULL;
        }
        else
            add_size_mismatch_error(error_list, it, "nvalues", nvalues);
    }
    /* Keeping data units filled with NULLs is OK, but the array is guaranteed to exist, so we must ensure it does.
     * Normally alloc_data() takes care of that, but since we take group's data as our own instead, the auxililary
     * arrays must be allocated explicitly. */
    if (!priv->units_data)
        priv->units_data = g_new0(GwyUnit*, nvalues);

    priv->valuemin = g_new(gdouble, nvalues);
    priv->valuemax = g_new(gdouble, nvalues);

    ok = TRUE;

fail:
    g_clear_object(&its[ITEM_UNIT_XY].value.v_object);
    it = its + ITEM_UNITS_DATA;
    if (it->value.v_object_array) {
        for (gsize i = 0; i < it->array_size; i++)
            g_clear_object(it->value.v_object_array + i);
        g_free(it->value.v_object_array);
    }
    it = its + ITEM_DATA_LABELS;
    assign_string_array(&it->value.v_string_array, it->array_size, NULL, 0);
    g_free(its[ITEM_XY].value.v_double_array);
    g_free(its[ITEM_DATA].value.v_double_array);

    return ok;
}

static GwySerializable*
serializable_copy(GwySerializable *serializable)
{
    GwyScatter *scatter = GWY_SCATTER(serializable);
    GwyScatter *copy = gwy_scatter_new_alike(scatter);
    alloc_data(copy, scatter->n, scatter->priv->nvalues);
    gwy_scatter_copy_data(scatter, copy);
    return GWY_SERIALIZABLE(copy);
}

static void
serializable_assign(GwySerializable *destination, GwySerializable *source)
{
    GwyScatter *destscatter = GWY_SCATTER(destination), *srcscatter = GWY_SCATTER(source);

    alloc_data(destscatter, srcscatter->n, srcscatter->priv->nvalues);
    gwy_scatter_copy_data(srcscatter, destscatter);
    copy_info(srcscatter, destscatter);
}

/**
 * gwy_scatter_copy:
 * @scatter: A XYZ scattered data to duplicate.
 *
 * Create a new XYZ scattered data as a copy of an existing one.
 *
 * This function is a convenience gwy_serializable_copy() wrapper.
 *
 * Returns: (transfer full):
 *          A copy of the XYZ scattered data.
 **/
GwyScatter*
gwy_scatter_copy(GwyScatter *scatter)
{
    /* Try to return a valid object even on utter failure. Returning NULL probably would crash something soon. */
    if (!GWY_IS_SCATTER(scatter)) {
        g_assert(GWY_IS_SCATTER(scatter));
        return g_object_new(GWY_TYPE_SCATTER, NULL);
    }
    return GWY_SCATTER(gwy_serializable_copy(GWY_SERIALIZABLE(scatter)));
}

/**
 * gwy_scatter_assign:
 * @destination: Target XYZ scattered data.
 * @source: Source XYZ scattered data.
 *
 * Makes one XYZ scattered data equal to another.
 *
 * This function is a convenience gwy_serializable_assign() wrapper.
 **/
void
gwy_scatter_assign(GwyScatter *destination, GwyScatter *source)
{
    g_return_if_fail(GWY_IS_SCATTER(destination));
    g_return_if_fail(GWY_IS_SCATTER(source));
    if (destination != source)
        gwy_serializable_assign(GWY_SERIALIZABLE(destination), GWY_SERIALIZABLE(source));
}

/**
 * SECTION: scatter
 * @title: GwyScatter
 * @short_description: Irregular two-dimensional data
 *
 * #GwyScatter represents general, i.e. possibly unevenly spaced, two-dimensional data, also called XYZ data. Each
 * point has X and Y coordinates, and then any number of data values, including none (representing just a set of
 * coordinates in the plane). Nevertheless, the number of data values is uniform over all the points. Unlike images
 * (#GwyField or #GwyNield), scattered data can also be empty, i.e. contain zero points.
 *
 * Many functions take a value index, usually denoted @i, and operate on the @i-th value in each point.
 **/

/**
 * GwyScatter:
 * @n: Number of points.
 *
 * Object representing scattered data.
 *
 * The #GwyScatter struct contains some public fields that can be directly accessed for reading.  To change them, you
 * must use the #GwyScatter methods.
 **/

/**
 * GwyScatterClass:
 *
 * Class of scattered data.
 **/

/**
 * GwyScatterFilterFunc:
 * @xy: Pointer to the XY coordinates of the point.
 * @values: Pointer to the array of all values for the point.
 * @user_data: (closure): User data passed to the driver function.
 *
 * Type of scattered data filtering function.
 *
 * Retrurns: %TRUE to keep the point; %FALSE to discard it.
 **/

/**
 * GwyScatterCompareFunc:
 * @xydataa: Coordinates and values of point a.
 * @xydatab: Coordinates and values of point b.
 * @user_data: (closure): User data passed to the driver function.
 *
 * Type of scattered data sorting function.
 *
 * Both @xydataa and @ydatab are packed blocks of nvalues+2 doubles, with XY first, followed by all data values for
 * the point. The format is the same as in gwy_scatter_pack() and gwy_scatter_set_from_packed().
 *
 * Retrurns: Negative value if a < b; zero if a = b; positive value if a > b.
 **/

/* 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 : */
