Skip to main content

cluster_marker_style 0.4.0 copy "cluster_marker_style: ^0.4.0" to clipboard
cluster_marker_style: ^0.4.0 copied to clipboard

Beautiful, count-aware, cached cluster marker icons for Google Maps. A drop-in styling/rendering layer for any map clustering algorithm.

cluster_marker_style #

Beautiful, count-aware, cached cluster marker icons for Google Maps — a one-line drop-in for any clustering algorithm.

This is a styling/rendering layer, not a clustering algorithm. You keep using your clustering package of choice (e.g. google_maps_cluster_manager_2); this package supplies the cluster icon — rendered sharply, cached, and good-looking by default.

Panning and zooming a map of 3,000 clustered points, switching between the soft, flat and outlined styles

The soft, flat and outlined styles at counts from 8 to 125k, on light and dark map backgrounds

Rendered by the package itself — regenerate with flutter test --update-goldens tool/readme_image_test.dart.

The problem #

Clustering packages decide which markers group together, then hand you an empty markerBuilder slot and walk away. To draw the cluster you end up re-writing ~40 lines of PictureRecorder / Canvas / TextPainter boilerplate — every project — with no caching (so the map janks on every camera move), wrong device-pixel-ratio handling (blurry on high-DPI screens), and one hardcoded color (a cluster of 5 looks identical to a cluster of 5000).

This is the still-open Flutter issue flutter/flutter#153092 ("Add support for custom cluster icon"). This package fills that gap.

Before / after #

Before — ~40 lines, no cache, hardcodedAfter — one line
markerBuilder: (cluster) async {
  final recorder = PictureRecorder();
  final canvas = Canvas(recorder);
  const size = 120.0;
  final paint = Paint()..color = Colors.blue;
  canvas.drawCircle(
      const Offset(size / 2, size / 2), size / 2, paint);
  final tp = TextPainter(
    text: TextSpan(
      text: cluster.count.toString(),
      style: const TextStyle(
          fontSize: 40, color: Colors.white),
    ),
    textDirection: TextDirection.ltr,
  )..layout();
  tp.paint(
      canvas,
      Offset((size - tp.width) / 2,
             (size - tp.height) / 2));
  final img = await recorder
      .endRecording()
      .toImage(size.toInt(), size.toInt());
  final data =
      await img.toByteData(format: ImageByteFormat.png);
  // ...and you STILL have no caching, no DPR
  // handling, and no count-aware colors.
  return Marker(
    markerId: MarkerId(cluster.getId()),
    position: cluster.location,
    icon: BitmapDescriptor.fromBytes(
        data!.buffer.asUint8List()),
  );
},
final renderer = ClusterRenderer(ClusterStyle.soft());

// ...

markerBuilder: clusterMarkerBuilder(
  renderer: renderer,
  devicePixelRatio:
      MediaQuery.of(context).devicePixelRatio,
),

Why it's worth a dependency #

Drawing a circle is trivial. The value is everything around it that developers re-implement badly or skip:

  • Caching — keyed on the visual outcome, not the raw count, so a cluster of 47 and 48 share one bitmap. This is the single biggest factor in a smooth map.
  • Device-correct rendering — rasterized at native resolution via imagePixelRatio, sharp on every screen, on Android, iOS, and web.
  • Count-aware defaults — color and size change with the count automatically, with zero tuning.
  • Beautiful defaults — clearly better out of the box than the red/blue circle everyone hand-writes.

Quick start (with google_maps_cluster_manager_2) #

Add the dependencies:

dependencies:
  cluster_marker_style_gmcm: ^0.1.0   # brings in cluster_marker_style
  google_maps_flutter: ^2.9.0
  google_maps_cluster_manager_2: ^3.0.0

Wire it up:

import 'package:cluster_marker_style_gmcm/cluster_marker_style_gmcm.dart';

// 1. Pick a style and create a renderer (reuse it — it holds the cache).
final renderer = ClusterRenderer(
  ClusterStyle.soft(
    tiers: const [
      CountTier(upTo: 10, color: Colors.blue),
      CountTier(upTo: 100, color: Colors.orange),
      CountTier(upTo: 1000, color: Colors.red),
    ],
  ),
);

// 2. Hand the one-line builder to your ClusterManager.
ClusterManager<Place>(
  places,
  _updateMarkers,
  markerBuilder: clusterMarkerBuilder(
    renderer: renderer,
    devicePixelRatio: MediaQuery.of(context).devicePixelRatio,
    onTap: (cluster) => debugPrint('Tapped ${cluster.count}'),
    // singleItemBuilder: (cluster) async => ...  // optional override
  ),
);

To avoid icons popping in on the first frame, warm the cache once the device pixel ratio is known (e.g. in didChangeDependencies):

renderer.precache(
  counts: List.generate(99, (i) => i + 2), // every cluster up to 100
  devicePixelRatio: MediaQuery.of(context).devicePixelRatio,
);

Omit tiers entirely to get the polished default ramp. See the runnable example/ app, which clusters a few thousand points and switches between styles live. (Add your own Google Maps API key — placeholders are in example/android/.../AndroidManifest.xml and example/web/index.html.)

Named styles #

Three polished, zero-config looks that read on both light and dark maps:

Style Look
ClusterStyle.soft() Cool→warm ramp, white text, thin translucent ring, soft shadow.
ClusterStyle.flat() Solid Material colors, crisp, no ring or shadow.
ClusterStyle.outlined() White fill with a bold count-aware colored ring + matching text.

Each is fully overridable, or build a ClusterStyle(...) from scratch with your own tiers, ClusterTextStyle, ClusterBorder, ClusterShadow, padding, and CountFormatter.

Shapes #

Icons are circles by default. With long labels — a custom CountFormatter, or large counts at a small size — circles shrink the text to fit. Switch to ClusterShape.pill to keep the text size and widen the icon instead; short labels still render as circles:

final style = ClusterStyle.soft().copyWith(shape: ClusterShape.pill);

Migrating from 0.3.x #

The adapters moved out of this package in 0.4.0. Swap the dependency and the import; the API is unchanged:

0.3.x import 0.4.0 dependency + import
cluster_marker_style/cluster_marker_style_gmcm.dart cluster_marker_style_gmcm → cluster_marker_style_gmcm/cluster_marker_style_gmcm.dart
cluster_marker_style/cluster_marker_style_fluster.dart cluster_marker_style_fluster → cluster_marker_style_fluster/cluster_marker_style_fluster.dart

Scope (and what this is not) #

This package styles cluster icons. Bring your own clustering algorithm. It is deliberately small and does exactly one thing well.

It is not a clustering algorithm, not a map widget, not an animation system, and not a viewport/diff manager. The core renderer depends only on google_maps_flutter (for BitmapDescriptor) — cluster_marker_style does not depend on any clustering package, so it never adds version constraints you didn't ask for. Clustering bridges are separate packages: the google_maps_cluster_manager_2 one is cluster_marker_style_gmcm.

There's also a fluster adapter, cluster_marker_style_fluster. fluster only runs the clustering algorithm — it hands back a flat list where isCluster marks an aggregate and gives you no rendering at all — so flusterMarkers(...) styles + caches those aggregates and delegates individual points to your builder:

import 'package:cluster_marker_style_fluster/cluster_marker_style_fluster.dart';

final markers = await flusterClusterMarkers<MapMarker>(
  fluster: fluster,
  bounds: [westLng, southLat, eastLng, northLat],
  zoom: zoom.round(),
  renderer: clusterRenderer,
  devicePixelRatio: MediaQuery.of(context).devicePixelRatio,
  pointMarkerBuilder: (p) async => Marker(
    markerId: MarkerId(p.markerId!),
    position: LatLng(p.latitude!, p.longitude!),
  ),
);

More clustering-tool adapters are planned (e.g. for flutter_map clustering tools). Because the core is independent, new adapters are thin bridges that don't touch it.

API surface #

Core (cluster_marker_style.dart): ClusterStyle (+ .soft/.flat/.outlined), CountTier, ClusterShape, ClusterTextStyle, ClusterBorder, ClusterShadow, CountFormatter (+ DefaultCountFormatter), ClusterRenderer, ClusterIconCache, ClusterCacheKey, and the tierFor / sizeFor helpers.

Adapter packages (each re-exports the core):

  • cluster_marker_style_gmcm → clusterMarkerBuilder(...) for google_maps_cluster_manager_2.
  • cluster_marker_style_fluster → flusterMarkers(...) / flusterClusterMarkers(...) for fluster.

License #

MIT — see LICENSE.

0
likes
160
points
156
downloads
screenshot

Documentation

API reference

Publisher

verified publisherkakadulabs.com

Weekly Downloads

Beautiful, count-aware, cached cluster marker icons for Google Maps. A drop-in styling/rendering layer for any map clustering algorithm.

Homepage
Repository (GitHub)
View/report issues

Topics

#maps #google-maps #markers #clustering

License

MIT (license)

Dependencies

flutter, google_maps_flutter

More

Packages that depend on cluster_marker_style