When there are many objects in a view, it can become difficult to keep an overview. Therefore you can introduce clustering to help reduce the clutter. Figure 1, “Clustered versus original model” contrasts the visualization of an original model with the visualization of a derivative transformed through clustering.

clusvsnonclus
Figure 1. Clustered versus original model

Configuring a ClusteringTransformer

To cluster objects in your model, you make use of a ClusteringTransformer. A ClusteringTransformer aggregates model elements that are close together into a cluster object. The cluster object is added to the working set instead of the individual elements. Model elements that are not part of a cluster are added as-is.

The clustering algorithm operates on the basis of a few parameters. You can specify those parameters when you create the ClusteringTransformer. You can define:

  • clusterSize: A measure to express the approximate cluster size in pixels. This size indicates the area of the cluster in screen space.

  • minimumPoints: the minimum number of points required to form a cluster. For example, when you set this parameter to 3, there will never be a cluster that only represents 2 points. Each cluster will at least contain 3 points.

Program: Install a simple ClusteringTransformer with custom parameters to a FeatureLayer
const layer = new FeatureLayer(model, {
  selectable: true,
  // Use the create function from the ClusteringTransformer module
  transformer: create({
    defaultParameters: {
      clusterSize: 100,
      minimumPoints: 4
    }
  })
});

You can also set a transformer to a FeatureLayer by updating its transformer property.

The following sections describe how to customize a ClusteringTransformer further.

Cluster shape provider

A ClusterShapeProvider determines the shape of the cluster based on its contained elements.

For example, if a cluster contains cities, you can choose to represent the cluster with a point location of the city with the largest population, as demonstrated by Program: A custom shape provider.

Program: A custom shape provider
class BiggestCityClusterShapeProvider extends ClusterShapeProvider{
  getShape(aComposingElements: Feature[]): Shape {
    let biggestCity;
    for (let i = 0; i < aComposingElements.length; i++) {
      const city = aComposingElements[i];
      if (!biggestCity || (city.properties as any).population > (biggestCity.properties as any).population) {
        biggestCity = city;
      }
    }
    if (biggestCity?.shape?.focusPoint) {
      return biggestCity?.shape?.focusPoint;
    }
    throw new Error("Could not determine the shape");
  };
}

const biggestCityClusterShapeProvider = new BiggestCityClusterShapeProvider();

Program: Configuring a custom shape provider shows how you can add a custom shape provider to the ClusteringTransformer.

Program: Configuring a custom shape provider
const clusteringTransformer = create({
  defaultParameters: {
    clusterSize: 100,
    minimumPoints: 4,
    clusterShapeProvider: biggestCityClusterShapeProvider
  }
});

If no parameters are specified, sensible defaults are used: a point at the location of the element closest to the center of mass of the contained elements is chosen as the cluster shape. That default prevents the positioning of clusters in illogical places, a cluster of cities placed in a nearby body of water, for instance.

Clustering by class

One of the key aspects of the clustering algorithm is its use of a Classifier.

The algorithm classifies the elements to be transformed according to a classifier. A Classifier returns a classification for a model element. This is a String employed by the algorithm to separate the elements. Two elements with the same classification may or may not be clustered together. Two elements with distinct classifications are never clustered together.

For example, if a model contains cities and you do not want cities to be clustered across country borders, the classifier can simply return the name of the country to which a city belongs. Program: A classifier that classifies cities according to country shows how to classify cities according to their countries.

Program: A classifier that classifies cities according to country
class CityClassifier extends Classifier{
  getClassification(aObject: Feature): string {
    const noClassification = "";
    if (aObject.properties && (aObject.properties as any).country) {
      return (aObject.properties as any).country;
    }
    return noClassification;
  };
}

const cityClassifier = new CityClassifier();

Classifications also allow more fine-grained control over the clustering algorithm. You can specify parameters per classification or classification group.

Program: Classification specific parameters sets a custom shape provider that will be used for all classifications. However, elements classified as "Belgium" will not be clustered, and some custom parameters are used for elements classified as "France". Note that for this to work, the Classifier and the configuration of the ClusteringTransformer must work in harmony. In the example, the Classifier should return "Belgium" for Belgian cities and "France" for French cities.

Program: Classification specific parameters
const transformer = create({
  classifier: cityClassifier,
  defaultParameters: {
    clusterShapeProvider: biggestCityClusterShapeProvider
  },
  classParameters: [{
    classification: "Belgium",
    parameters: {
      noClustering: true
    }
  }, {
    classification: "France",
    parameters: {
      clusterSize: 100,
      minimumPoints: 4
    }
  }]
});

You can choose to specify an exact classification, but you can also use a function. Functions allow you to specify parameters for multiple classes. The order in which you specify parameters per classification or classification group is important. The parameters of the first classification match are used.

Program: Classification specific parameters combining exact matching and a function shows a more complex configuration using exact matching and a function.

Program: Classification specific parameters combining exact matching and a function
const complexTransformer = create({
  classifier: cityClassifier,
  classParameters: [{
    classMatcher: function(classification): boolean {
      return classification.toLowerCase() === "belgium";
    },
    parameters: {
      clusterSize: 100,
      minimumPoints: 4
    }
  }, {
    classification: "Brazil",
    parameters: {
      noClustering: true
    }
  }]
});

Scale-dependent clustering settings

For certain datasets, it makes sense to apply another clustering setting when the scale of the view changes.

To set up different clustering settings for various view scales, you can combine multiple ClusteringTransformer instances, each with their own settings, into one single scale-dependent transformer. This is done by calling the createScaleDependent method.

Program: Scale-dependent clustering
// When zoomed in, avoid grouping events happening in different countries in the same clusters
// and apply some custom configurations for elements classified as "Belgium" and "France".
const zoomedInTransformer = create({
  classifier: cityClassifier,
  classParameters: [{
    classification: "Belgium",
    parameters: {
      noClustering: true
    }
  }, {
    classification: "France",
    parameters: {
      clusterSize: 100,
      minimumPoints: 4
    }
  }]
});

// When zoomed out, all the events can be clustered together.
// Otherwise, we would end up with overlapping clusters as the countries become rather small
const zoomedOutTransformer = create();

// Switching between the two clustering approaches should happen at a scale 1 : 25 000 000
const scaleDependentTransformer = createScaleDependent({
  levelScales: [1 / 25000000],
  clusteringTransformers: [zoomedOutTransformer, zoomedInTransformer]
});