Clustering

When rendering a large number of points in a map, symbolizing each point individually is often counterproductive since symbols frequently overlap, making it difficult to distinguish between points. Symbolizing multiple points with a single symbol is often used to resolve this issue by providing a more aesthetic, efficient, and usable rendering solution. This process is called clustering.

Clustering identifies groups of points in a layer that are within a given cluster distance. The cluster distance is dependent on the scale at which the map is displayed; as you zoom in, fewer points are clustered and as you zoom out, more points are clustered. Clustering can be applied to a GraphicsLayer or FeatureLayer.

The two techniques for clustering graphics are as follows:

  • Using the FlareClusterer
  • Extending the GraphicsClusterer to create a custom clustering solution

Both techniques can be used with a GraphicsLayer or FeatureLayer.

Using the FlareClusterer

A FlareClusterer can be added to a GraphicsLayer or FeatureLayer using the following XAML:

<esri:GraphicsLayer ID="MyGraphicsLayer">
   <esri:GraphicsLayer.Clusterer>
      <esri:FlareClusterer />
   </esri:GraphicsLayer.Clusterer>
</esri:GraphicsLayer>

Groups of points that form a cluster are symbolized using either a flare cluster or large cluster symbol. The default flare cluster symbol consists of a red circle with a white border and a number representing the number of clustered points. The symbol is also animated. When you hover the pointer over the feature in the map, the symbol flares out to display each point as a single red circle attached to the cluster.

The default large cluster symbol is rendered using a gradient color between red and yellow, and the feature count the cluster represents is drawn on top of the symbol. The large cluster symbol is not interactive. The MaximumFlareCount property on the FlareClusterer determines when a flare or large cluster symbol is used (see the table below).

Flare cluster symbols

You can modify the FlareClusterer in XAML using the following properties (default values are listed):

FlareClusterer propertyDescription

FlareBackground

Background color (fill) of the flare symbol. Default = Red.

FlareForeground

Text and boundary color of the flare symbol. Default = White.

MaximumFlareCount

The maximum number of clustered features to be represented by a flare cluster. If the number of features is greater than this value, a large cluster is used to render the features. The large cluster displays the number of features it represents. Its size is scaled to the number of features, and the color is defined by a gradient (see Gradient property below). Default = 10.

Radius

The radius within which features will be clustered. Defined in pixels. Default = 20.

Gradient

The LinearGradientBrush used to render large clusters. Default = LinearGradientBrush; MappingMode = RelativeToBoundingBox; GradientStop1: Offset = 0, Argb = 127,255,255,0, GradientStop2: Offset = 1, Argb = 127,255,0,0.

The following code example illustrates how to modify FlareClusterer properties in XAML. The result will generate flare symbols with a yellow background with gray border and text, and a blue gradient symbol for the large clusters. Flare symbols represent, at a maximum, five features with a 15 pixel radius. A green circle symbol has been applied to single graphic features. See the Simple Clusterer sample in the Interactive SDK for a functional example.

Custom flare clusterer

<Grid x:Name="LayoutRoot" Background="White">
    <Grid.Resources>
        <LinearGradientBrush x:Key="BlueGradient" MappingMode="RelativeToBoundingBox" >
            <GradientStop Color="#990011FF" Offset="0"/>
            <GradientStop Color="#990055FF" Offset="0.25"/>
            <GradientStop Color="#990099FF" Offset="0.5"/>
            <GradientStop Color="#9900CCFF" Offset="0.75"/>
            <GradientStop Color="#9900FFFF" Offset="1"/>
        </LinearGradientBrush>
    </Grid.Resources>
    <esri:Map x:Name="MyMap">
        <esri:Map.Layers>
            <esri:GraphicsLayer ID="MyGraphicsLayer">
                <esri:GraphicsLayer.Clusterer>
                    <esri:FlareClusterer 
                        FlareBackground="Yellow"
                        FlareForeground="#99000000"
                        MaximumFlareCount="5" Radius="15" 
                        Gradient="{StaticResource BlueGradient}" />
                </esri:GraphicsLayer.Clusterer>
            </esri:GraphicsLayer>
        </esri:Map.Layers>
    </esri:Map>
</Grid>

Extending the GraphicsClusterer

To customize the look, feel, and behavior of clustering, you need to create a custom class that derives from ESRI.ArcGIS.Client.GraphicsClusterer and override the OnCreateGraphic() method to return a cluster graphic. The clustered features are passed to the OnCreateGraphic method in a graphic collection. Interrogate the collection and graphic features to determine how to create a clustered graphic with a symbol. The FlareClusterer returns either a flare cluster symbol or a large cluster symbol.

The following code example generates a single symbol for any graphic collection that has more than one feature. The cluster symbol size and color are calculated, and values in an attribute column in the GraphicsLayer or FeatureLayer on which the custom clusterer is applied will be aggregated. The sum total of the values will be displayed on the cluster symbol. For brevity, only the custom cluster class source code is provided here. To view the complete example, see the Custom Clusterer sample in the Interactive SDK.

public class SumClusterer : GraphicsClusterer
    {
        public SumClusterer()
        {
            MinimumColor = Colors.Red;
            MaximumColor = Colors.Yellow;
            SymbolScale = 1;
            base.Radius = 50;
        }

        public string AggregateColumn { get; set; }
        public double SymbolScale { get; set; }
        public Color MinimumColor { get; set; }
        public Color MaximumColor { get; set; }

        protected override Graphic OnCreateGraphic(GraphicCollection cluster, MapPoint point, int maxClusterCount)
        {
            if (cluster.Count == 1) return cluster[0];
            Graphic graphic = null;

            double sum = 0;

            foreach (Graphic g in cluster)
            {
                if (g.Attributes.ContainsKey(AggregateColumn))
                {
                    try
                    {
                        sum += Convert.ToDouble(g.Attributes[AggregateColumn]);
                    }
                    catch { }
                }
            }
            double size = (sum + 450) / 30; 
            size = (Math.Log(sum * SymbolScale / 10) * 10 + 20);
            if (size < 12) size = 12;
            graphic = new Graphic() { Symbol = new ClusterSymbol() { Size = size }, Geometry = point };
            graphic.Attributes.Add("Count", sum);
            graphic.Attributes.Add("Size", size);
            graphic.Attributes.Add("Color", InterpolateColor(size - 12, 100));
            return graphic;
        }

        private static Brush InterpolateColor(double value, double max)
        {
            value = (int)Math.Round(value * 255.0 / max);
            if (value > 255) value = 255; 
            else if (value < 0) value = 0;
            return new SolidColorBrush(Color.FromArgb(127, 255, (byte)value, 0));
        }
    }

internal class ClusterSymbol : ESRI.ArcGIS.Client.Symbols.MarkerSymbol
{
    public ClusterSymbol()
    {
        string template = @"
        <ControlTemplate xmlns=""http://schemas.microsoft.com/client/2007""
                         xmlns:x=""http://schemas.microsoft.com/winfx/2006/xaml"" >
            <Grid IsHitTestVisible=""False"">
                <Ellipse
                    Fill=""{Binding Attributes[Color]}""
                    Width=""{Binding Attributes[Size]}""
                    Height=""{Binding Attributes[Size]}"" />
                <Grid HorizontalAlignment=""Center"" VerticalAlignment=""Center"">
                    <TextBlock
                        Text=""{Binding Attributes[Count]}""
                        FontSize=""9"" Margin=""1,1,0,0"" FontWeight=""Bold""
                        Foreground=""#99000000"" />
                    <TextBlock
                        Text=""{Binding Attributes[Count]}""
                        FontSize=""9"" Margin=""0,0,1,1"" FontWeight=""Bold""
                        Foreground=""White"" />
                </Grid>
            </Grid>
        </ControlTemplate>";

        this.ControlTemplate = System.Windows.Markup.XamlReader.Load(template) as ControlTemplate;
    }

    public double Size { get; set; }
    public override double OffsetX
    {
        get
        {
            return Size / 2;
        }
        set
        {
            throw new NotSupportedException();
        }
    }
    public override double OffsetY
    {
        get
        {
            return Size / 2;
        }
        set
        {
            throw new NotSupportedException();
        }
    }
}