When the view displays many objects, it can become difficult to keep an overview. You can set up the LuciadRIA clustering capability 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. It adds the cluster object to the working set instead of the individual elements. It adds model elements that aren’t part of the cluster to the working set as-is.

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

  • clusterSize: A measure to express the rough 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, each cluster contains 3 points at least, never just 2 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, in a cluster with cities, you can represent the cluster with a point location of the city with the largest population. See Program: A custom shape provider for a demonstration.

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 you don’t specify any parameters, the clustering transformer uses sensible defaults: it places the point with the cluster shape at the location of the element closest to the center of mass of all clustered elements. 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 you don’t want the cities in your model clustered across country borders, the classifier can 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. It won’t cluster elements classified as "Belgium", though, and it uses custom parameters 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 Classifierreturns "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 specify an exact classification, but you can also decide to use a function. You can specify parameters for multiple classes if you use functions. The order of the 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 ClusteringTransformer instances, each with its own settings, into a single scale-dependent transformer. You call the createScaleDependent method for this.

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]
});