Clustered point series

Clustered point series (ClusteredPointSeries) is basically a regular map point series (MapPointSeries) except with an added capability of automatically closely located bullets into groups, so they do not overlap.

The groups would update automatically when zooming and panning the map.

Creating series

We create clustered point series, configure itself and its bullets, set data, exactly the same way as regular map point series, except instead of using MapPointSeries class, we use ClusteredPointSeries.

let pointSeries = chart.series.push(
  am5map.ClusteredPointSeries.new(root, {
    // ...
  })
);
var pointSeries = chart.series.push(
  am5map.ClusteredPointSeries.new(root, {
    // ...
  })
);

MORE INFOPlease refer to "Map point series" documentation for info on how to configure series markers (bullets), series itself, as well as set data.

Configuring

Minimal distance

Clustered point series automatically groups bullets that are closer than 20 pixels between each other.

We can change this value using series' minDistance setting:

let pointSeries = chart.series.push(
  am5map.ClusteredPointSeries.new(root, {
    minDistance: 30
  })
);
var pointSeries = chart.series.push(
  am5map.ClusteredPointSeries.new(root, {
    minDistance: 30
  })
);

Scatter settings

When the map is nearing its maximum allowed zoom level (95% by default), all groups are exploded to reveal all bullets, even if they are located closer than minDistance.

In such case, in order for them to not overlap, they will be automatically scattered.

We can control the distance and presumed radius of such bullet so they are scattered in such a way that they do not overlap, or overlap only partially, using series settings scatterDistance and scatterRadius respectively.

We can also control the zoom point at which the scattering is applied using stopClusterZoom setting (default: 0.95).

let pointSeries = chart.series.push(
  am5map.ClusteredPointSeries.new(root, {
    minDistance: 30,
    scatterDistance: 10,
    scatterRadius: 10,
    stopClusterZoom: 0.9
  })
);
var pointSeries = chart.series.push(
  am5map.ClusteredPointSeries.new(root, {
    minDistance: 30,
    scatterDistance: 10,
    scatterRadius: 10,
    stopClusterZoom: 0.9
  })
);

Group segregation

In some cases we might want to restrict bullets to group only with a set of other bullets.

For example, we might want to force bullets from one continent to group only with bullets from the same continent.

That's where series setting groupIdField comes in.

Using it, we can specify which field on series' data holds group id.

let pointSeries = chart.series.push(
  am5map.ClusteredPointSeries.new(root, {
    groupIdField: "group"
  })
);
var pointSeries = chart.series.push(
  am5map.ClusteredPointSeries.new(root, {
    groupIdField: "group"
  })
);

Group bullet

We also need to define a special bullet to be used when several regular bullets are grouped.

It is defined like regular bullet in series, except instead of pushing it into bullets, we set it as a series' clusteredBullet setting.

As with any regular bullets, it should return a Bullet object, and can contain any visual elements.

Basic bullet

Let's start with a very basic bullet, which shows a circle:

pointSeries.set("clusteredBullet", function(root) {
  let circle = am5.Circle.new(root, {
    radius: 8,
    tooltipY: 0,
    fill: am5.color(0xff8c00)
  });

  return am5.Bullet.new(root, {
    sprite: circle
  });
});
pointSeries.set("clusteredBullet", function(root) {
  var circle = am5.Circle.new(root, {
    radius: 8,
    tooltipY: 0,
    fill: am5.color(0xff8c00)
  });

  return am5.Bullet.new(root, {
    sprite: circle
  });
});

Multi-element bullet

Bullet can contain any elements, including a Container with multiple children.

Let's enhance the bullet to use multiple concentric circles.

pointSeries.set("clusteredBullet", function(root) {
  const container = am5.Container.new(root, {
});

  const circle1 = container.children.push(am5.Circle.new(root, {
    radius: 8,
    tooltipY: 0,
    fill: am5.color(0xff8c00)
  }));

  const circle2 = container.children.push(am5.Circle.new(root, {
    radius: 12,
    fillOpacity: 0.3,
    tooltipY: 0,
    fill: am5.color(0xff8c00)
  }));

  const circle3 = container.children.push(am5.Circle.new(root, {
    radius: 16,
    fillOpacity: 0.3,
    tooltipY: 0,
    fill: am5.color(0xff8c00)
  }));

  return am5.Bullet.new(root, {
    sprite: container
  });
});
pointSeries.set("clusteredBullet", function(root) {
  var container = am5.Container.new(root, {});

  var circle1 = container.children.push(am5.Circle.new(root, {
    radius: 8,
    tooltipY: 0,
    fill: am5.color(0xff8c00)
  }));

  var circle2 = container.children.push(am5.Circle.new(root, {
    radius: 12,
    fillOpacity: 0.3,
    tooltipY: 0,
    fill: am5.color(0xff8c00)
  }));

  var circle3 = container.children.push(am5.Circle.new(root, {
    radius: 16,
    fillOpacity: 0.3,
    tooltipY: 0,
    fill: am5.color(0xff8c00)
  }));

  return am5.Bullet.new(root, {
    sprite: container
  });
});

Bullet with count

We can also add a Label element to our bullet to show the number of regular bullets that went into a group bullet.

Since every bullet element has also access to bullet's data item, we can use data placeholders to dynamically populate our label.

pointSeries.set("clusteredBullet", function(root) {
  const container = am5.Container.new(root, {});

  const circle1 = container.children.push(am5.Circle.new(root, {
    radius: 8,
    tooltipY: 0,
    fill: am5.color(0xff8c00)
  }));

  const circle2 = container.children.push(am5.Circle.new(root, {
    radius: 12,
    fillOpacity: 0.3,
    tooltipY: 0,
    fill: am5.color(0xff8c00)
  }));

  const circle3 = container.children.push(am5.Circle.new(root, {
    radius: 16,
    fillOpacity: 0.3,
    tooltipY: 0,
    fill: am5.color(0xff8c00)
  }));

  var label = container.children.push(am5.Label.new(root, {
    centerX: am5.p50,
    centerY: am5.p50,
    fill: am5.color(0xffffff),
    populateText: true,
    fontSize: "8",
    text: "{value}"
  }));

  return am5.Bullet.new(root, {
    sprite: container
  });
});
pointSeries.set("clusteredBullet", function(root) {
  var container = am5.Container.new(root, {});

  var circle1 = container.children.push(am5.Circle.new(root, {
    radius: 8,
    tooltipY: 0,
    fill: am5.color(0xff8c00)
  }));

  var circle2 = container.children.push(am5.Circle.new(root, {
    radius: 12,
    fillOpacity: 0.3,
    tooltipY: 0,
    fill: am5.color(0xff8c00)
  }));

  var circle3 = container.children.push(am5.Circle.new(root, {
    radius: 16,
    fillOpacity: 0.3,
    tooltipY: 0,
    fill: am5.color(0xff8c00)
  }));

  var label = container.children.push(am5.Label.new(root, {
    centerX: am5.p50,
    centerY: am5.p50,
    fill: am5.color(0xffffff),
    populateText: true,
    fontSize: "8",
    text: "{value}"
  }));

  return am5.Bullet.new(root, {
    sprite: container
  });
});

Drill-down

Now, let's set up drill-down, so that when you click on a group bullet, the map zooms just enough so that all elements in that group become visible separately.

For that we need to set up click event on our bullet, which would in turn invoke series' zoomToCluster() method.

zoomToCluster() method accepts group bullet's data item as a parameter.

It also has an optional second Boolean parameter, which indicates whether we want to rotate the map so that it is centered on the group.

It is recommended to use true as a second parameter on a map with Orthographic (globe) projection.

pointSeries.set("clusteredBullet", function(root) {
  const container = am5.Container.new(root, {
    cursorOverStyle:"pointer"
  });

  const circle1 = container.children.push(am5.Circle.new(root, {
    radius: 8,
    tooltipY: 0,
    fill: am5.color(0xff8c00)
  }));

  const circle2 = container.children.push(am5.Circle.new(root, {
    radius: 12,
    fillOpacity: 0.3,
    tooltipY: 0,
    fill: am5.color(0xff8c00)
  }));

  const circle3 = container.children.push(am5.Circle.new(root, {
    radius: 16,
    fillOpacity: 0.3,
    tooltipY: 0,
    fill: am5.color(0xff8c00)
  }));

  var label = container.children.push(am5.Label.new(root, {
    centerX: am5.p50,
    centerY: am5.p50,
    fill: am5.color(0xffffff),
    populateText: true,
    fontSize: "8",
    text: "{value}"
  }));

  container.events.on("click", function(e) {
    pointSeries.zoomToCluster(e.target.dataItem);
  });

  return am5.Bullet.new(root, {
    sprite: container
  });
});
pointSeries.set("clusteredBullet", function(root) {
  var container = am5.Container.new(root, {
    cursorOverStyle:"pointer"
  });

  var circle1 = container.children.push(am5.Circle.new(root, {
    radius: 8,
    tooltipY: 0,
    fill: am5.color(0xff8c00)
  }));

  var circle2 = container.children.push(am5.Circle.new(root, {
    radius: 12,
    fillOpacity: 0.3,
    tooltipY: 0,
    fill: am5.color(0xff8c00)
  }));

  var circle3 = container.children.push(am5.Circle.new(root, {
    radius: 16,
    fillOpacity: 0.3,
    tooltipY: 0,
    fill: am5.color(0xff8c00)
  }));

  var label = container.children.push(am5.Label.new(root, {
    centerX: am5.p50,
    centerY: am5.p50,
    fill: am5.color(0xffffff),
    populateText: true,
    fontSize: "8",
    text: "{value}"
  }));

  container.events.on("click", function(e) {
    pointSeries.zoomToCluster(e.target.dataItem);
  });

  return am5.Bullet.new(root, {
    sprite: container
  });
});

Examples

World map with clustered points

See the Pen Map with clustered points by amCharts team (@amcharts) on CodePen.

US map with segregated clusters by state

See the Pen US map with state-grouped clustered points by amCharts team (@amcharts) on CodePen.