Git Product home page Git Product logo

use-supercluster's Introduction

useSupercluster

A hook for using Supercluster with React.

const { clusters, supercluster } = useSupercluster({
  points: [],
  bounds: [
    -1.2411810957931664,
    52.61208435908725,
    -1.0083656811012531,
    52.64495957533833
  ],
  zoom: 12,
  options: { radius: 75, maxZoom: 20 }
});

Installation

You will need to install Supercluster as a peer dependency of this package.

yarn add supercluster use-supercluster

Examples

This package contains an example along with tests, but full examples with instructions in the most popular mapping libraries for React can be found below.

Mapbox

Full instructions and an example can be found here.

Google Maps

Full instructions and an example can be found here.

Leaflet

Full instructions and an example can be found here.

Configuration

The 'options' property passed to useSupercluster supports Supercluster's options and additionally includes support for the disableRefresh option."

Map & Reduce Options

As an example, you can use map and reduce to keep track of a total value summed up from across the points. In this case we have the total cost, the max severity, plus another count (which is redundant because Supercluster gives us the point_count property).

const options = {
  radius: 75,
  maxZoom: 20,
  map: props => ({
    cost: props.cost,
    severity: props.severity,
    count: 1
  }),
  reduce: (acc, props) => {
    acc.count += 1;
    acc.cost += props.cost;
    acc.severity = Math.max(acc.severity, props.severity);
    return acc;
  }
};

I found map and reduce a little confusing! The value returned from map of the first point is used as the initial value passed as the accumulator to the reduce function. The only props you have available in reduce are the ones returned from map. You technically don't need to return a value from reduce (it's not used), but instead need to mutate the accumulator object.

Then these accumulated properties can be used and are available on each cluster:

<ul>
  {clusters.map(point => {
    const properties = point.properties || {};
    if (properties.cluster) {
      return (
        <li key={point.id}>
          <h2>Points: {properties.point_count}</h2>
          <p>Cost: {properties.cost.toFixed(2)}</p>
          <p>Severity: {properties.severity}</p>
          <p>Count: {properties.count}</p>
        </li>
      );
    } else {
      return <li key={properties.crimeId}>{properties.category}</li>;
    }
  })}
</ul>

disableRefresh

With the disableRefresh option, clusters returned from useSupercluster will be based on previous data despite zoom/bounds/points having changed. For example, using isFetching from useQuery, we can set disableRefresh so that Supercluster doesn't perform clustering until we've successfully fetched new data.

const Component = () => {
  const { data, isFetching } = useQuery("markers", getMarkers());

  const { clusters, supercluster } = useSupercluster({
    /* ... normal options passed to hook ... */
    disableRefresh: isFetching
  });

  return (
    <Map>
      {clusters.map(cluster => {
        return <Marker />;
      })}
    </Map>
  );
};

use-supercluster's People

Contributors

leighhalliday avatar dependabot[bot] avatar wherehows avatar kevinkreps avatar nfy-sg avatar

Stargazers

Firat Ciftci avatar  avatar Dylan Barkowsky avatar fabi.s avatar Haldun YILDIZ avatar Ahmad Aizat Bin Adam avatar Hy Huynh avatar Dmitrii Pikulin avatar chuan.yue avatar wangxingkang avatar Forrest Rice avatar Liam avatar Karel Nguyen avatar Faisal TariQ avatar Ryan Yosua avatar  avatar Ronald Goedeke avatar Ariel Falduto avatar  avatar Iv Po avatar Wade avatar  avatar Tanner Varrelman avatar Anthony You avatar Paulo Miguel avatar  avatar Evon Scott avatar Omar Khatib avatar Simon Betton avatar Ben Tyler avatar Dan Levy avatar  avatar Diego Jesus Ayala Peña avatar Kartik Setia avatar Richard Unterberg avatar Dan avatar Lukas Stuart-Fry avatar Nima Karimi avatar Ahmad Ali avatar Johnatan Dias avatar Nícolas Oliveira avatar  avatar Hugo Cárdenas avatar Kacper Rożniata avatar  avatar  avatar Dusan Jovanov avatar Ömercan Balandı avatar K Bloom avatar Larry Agbana avatar  avatar Jesse Okeya avatar Alex Hunt avatar Arjun Rao avatar ilhan Tekir avatar José Filipe Ferreira avatar Nathan Hutchision avatar Martins Obayomi avatar Vsevolod Pechenin avatar santana2000 avatar Feffery avatar Jason avatar Nuno Lemos avatar Ruslan Zainetdinov avatar  avatar Jon Yoo avatar uvacoder avatar Erinç Polat avatar Imran Khan avatar Sergiu Ojoc avatar Hasan Mumin avatar Jack Clackett avatar John Zambrano avatar Scott Nimos avatar Shun Kakinoki avatar Spencer Pauly avatar  avatar Nando avatar Jack avatar Mithushan Jalangan avatar Jonas Galvez avatar Thiago Vieira  avatar Mike Stecker avatar José Adilson avatar Durnea Mădălin avatar Jacob Devera avatar Jim Johnston avatar  avatar Felipe Rodrigues avatar Son Tran avatar alwayrun avatar Umalat avatar Chandu avatar Wesley Brian Lachenal avatar Calle Helmertz avatar Olexandr Podopryhora avatar Thiph avatar Luca Morosini avatar Wilson Gichu avatar Emery Muhozi avatar

Watchers

 avatar James Cloos avatar Nader Naderi avatar Lukas Stuart-Fry avatar Mattia M. avatar John Zambrano avatar  avatar

use-supercluster's Issues

type error getClusterExpansionZoom

I have a hard time making Typescript happy (warning - im new to typescript)

Argument of type 'string | number | undefined' is not assignable to parameter of type 'number'.
  Type 'undefined' is not assignable to type 'number'.ts(2345)

getClusterExpansionZoom(cluster.id)

I assume this is because of this

// index.d.ts
type PointFeature<P> = GeoJSON.Feature<GeoJSON.Point, P>;  // defines [id] as   string | number

So should it rather be something like this

getClusterExpansionZoom(cluster.properties.cluster_id) // makes tslint happy

This tripped me of while doing your tutorial which is otherwise fantastic.

Thank you

unbond data with same coordinate

Hello, first of all, congratulations, i saw your youtube video, and it helped me a lot on the implementation

But i need guidance regarding when we achive the maxZoom, but we have to properties with same coordinates, how could desustructure them?

for example: following exactly your video about cluster, i could implement successfully, but i may face very rare cases, where even with the maxZoom, some applications have the same exact coordinate, but, they are different application, with different data,

image

how could i unbod them and know exaclty what i have inside this cluster?

searching i found getChildren(clusterId), but is not bringing me the children yet

Content in marker

I'm loving this hook but I'm finding difficulty in doing a few things.

I want to make a map in which the final markers are photos and titles, eg:

image

The clusters, different from your examples, would have a photo, a title and a number count. Eg:

image

How I achieve this? I pick a random item from that cluster and populate the cluster marker.

const items = supercluster.getLeaves(cluster.id);const randomItem = items[Math.floor(Math.random() * items.length)].properties;

The issue is: every time I scroll the mouse, a new item is populating my marker. So, in every scroll, my randomizing event runs again.

I don't even know where to start in order to fix this and set a fixed image and title for each cluster marker and I can't find the solution within your code.

My code (I took a few unnecessary lines out trying to get it smaller, like Styled Components styling):

// (...) import everything

// I set some fixed numbers for easier tweaking later
const pinMaxSize = 80;
const clusterRadius = 200;

const Map = ({ data: mapData, category }) => {
	// Hooks
	const [hasErrors, setErrors] = useState(false);
	const [mapOptions, setOptions] = useState({});
	const [viewport, setViewport] = useState({
		latitude: -22.63937652409258,
		longitude: -49.75508398437603,
		width: '100%',
		height: '100%',
		zoom: 6,
		bearing: 0,
		pitch: 0,
	});
	const mapRef = useRef();

	// (...) here a fetch some data from json, working properly

	// Setting up points
	const points = mapData.map(item => ({
		type: 'Feature',
		properties: {
			cluster: false,
			id: item.id,
			title: item.title,
			photo: item.photo,
		},
		geometry: {
			type: 'Point',
			coordinates: [
				parseFloat(item.long),
				parseFloat(item.lat),
			],
		},
	}));

	// setting up clusters
	const bounds = mapRef.current
		? mapRef.current
			.getMap()
			.getBounds()
			.toArray()
			.flat()
		: null;

	const { clusters, supercluster } = useSupercluster({
		points,
		bounds,
		zoom: viewport.zoom,
		options: { radius: clusterRadius, maxZoom: 20 },
	});

	// Rendering map
	if (hasErrors) {
		return <Loading error={hasErrors} />;
	}

	if (Object.keys(mapOptions).length > 0) {
		return (
			<Wrapper>
				<MapGL
					{...viewport}
					ref={mapRef}
					mapboxApiAccessToken={process.env.REACT_APP_MAPBOX_TOKEN}
					// maxZoom={10}
					// minZoom={4.5}
					onViewportChange={(newViewport) => {
						setViewport({ ...newViewport });
					}}
				>
					{/* Clusters */}
					{clusters.map((cluster) => {
						const [longitude, latitude] = cluster.geometry.coordinates;
						const {
							cluster: isCluster,
							point_count: pointCount,
						} = cluster.properties;

						if (isCluster) {
							const items = supercluster.getLeaves(cluster.id);
							const randomItem = items[Math.floor(Math.random() * items.length)].properties;
							return (
								<Cluster
									key={`cluster-${cluster.id}`}
									latitude={latitude}
									longitude={longitude}
									totalPoints={points.length}
									pointCount={pointCount}
									supercluster={supercluster}
									cluster={cluster}
									setViewport={setViewport}
									viewport={viewport}
									size={pinMaxSize}
									randomItem={randomItem}
								/>
							);
						}
						return (
							<Pin
								key={`item-${cluster.properties.id}`}
								item={cluster.properties}
								latitude={latitude}
								longitude={longitude}
								size={pinMaxSize}
								category={category}
							/>
						);
					})}
				</MapGL>
			</Wrapper>
		);
	}

	return <Loading />;
};

export default Map;

The Cluster component:

const Cluster = ({
	latitude,
	longitude,
	pointCount,
	supercluster,
	cluster,
	setViewport,
	viewport,
	size,
	randomItem,
}) => (
	<Marker
		key={`cluster-item-${cluster.id}`}
		latitude={latitude}
		longitude={longitude}
		offsetLeft={-size / 2}
		offsetTop={-size / 2}
	>
		<MarkerCluster
			as="button"
			size={size}
			onClick={() => {
				const expansionZoom = Math.min(
					supercluster.getClusterExpansionZoom(cluster.id),
					20,
				);

				setViewport({
					...viewport,
					latitude,
					longitude,
					zoom: expansionZoom,
					// transitionInterpolator: new FlyToInterpolator({
					// 	speed: 2,
					// }),
					// transitionDuration: 'auto',
				});
			}}
		>
			<Photo photo={randomItem.photo} size={size}>
				<Count><span>{pointCount}</span></Count>
			</Photo>
			<Name size={size}>{randomItem.title}</Name>
		</MarkerCluster>
	</Marker>
);

(I can't make the transitionInterpolator work but that's a different issue).

The Pin component:

const Pin = ({
	item, latitude, longitude, size, category,
}) => (
	<Marker
		key={`${item.id}-${uuid()}`}
		latitude={latitude}
		longitude={longitude}
		offsetLeft={-size / 2}
		offsetTop={-size / 2}
	>
		<MarkerLink
			size={size}
			to={`/${category}/${item.id}`}
		>
			<Photo photo={item.photo} size={size} />
			<Name size={size}>{item.title}</Name>
		</MarkerLink>
	</Marker>
);

seems to be returning `any` from hook

image

I attempted to explicitly set the generic types of the hook, but it is still returning any. Is this something that can be fixed here, or is it an underlying problem with the supercluster library

Add compatibility with Supercluster v8

Hi,

Supercluster v8 has drop support to CommonJS and this has broken this packages when is used with Supercluster v8. Is interesting add support to ESM because Supercluser v8 has a lot of performance improvements.

The error is:

Error: require() of ES Module /Users/rodrigotome/Sites/nestar-storefront/node_modules/supercluster/index.js from /Users/rodrigotome/Sites/nestar-storefront/node_modules/use-supercluster/dist/use-supercluster.cjs.development.js not supported.
Instead change the require of index.js in /Users/rodrigotome/Sites/nestar-storefront/node_modules/use-supercluster/dist/use-supercluster.cjs.development.js to a dynamic import() which is available in all CommonJS modules.

Release notes of Supercluster v8 -> https://github.com/mapbox/supercluster/releases/tag/v8.0.0.

Maybe the only fix that the packages need is to set that it is a module in the package.json

Thanks

useSupercluster gives back empty array when re-rendering points that have the same lat/lng values

I'm using useSupercluster like below

  const { clusters } = useSupercluster({
    points,
    bounds,
    zoom,
    options: {
      radius: 75,
      maximumZoom
    }
  });

I have functionality that happens onClick of a map pin. It adds a selected: true property to the selected point. useSupercluster returns an empty array as the value for clusters once this operation is complete.

Notes:

  1. Nothing else changes (bounds, zoom, options)
  2. Altering the values for the other inputs (bounds, zoom) kicks useSupercluster into gear and it returns what is expected (changing by zooming or shifting map)
  3. This is occurring on a set of points that have the same exact lat/long values.
  4. This does not occur on a set of points that do not share lat/long values.
  5. This happens when clicking on a single point (i.e. no actual "clusters")

Any help is appreciated!
Thanks!

Hook always create new cluster

There is a problem in comparing options

!dequal(
  (superclusterRef.current as typeof superclusterRef.current & {
    options: typeof options;
  }).options,
  options
)

It is always false since options in Supercluster are different from options from props

useSuperCluster hook returns empty array when data was passed in props.

I can replicate the example from video tutorial, but when I request data with fetch from parent component and pass it as a prop to my map(child) component to create clusters, clusters array always end up being empty. It's the same thing, but basically without useSwr. Is useSwr a requirement to run use-supercluster? If not, how would you do it?

That's the way I tried to implement it

const nodes = props.flight?.samples ? props.flight.samples : [];
  const heatPositions = nodes.map( node => ({
    lat: node.latitude,
    lng: node.longitude,
    weight: 2
  }));
  console.log('nodes: ', nodes);
  const points = nodes.map( node => ({
    type: 'Feature',
    properties: {
      cluster: false,
      nodeId: node.timestamp,
      value: node.values
    },
    geometry: { type: 'Point', coordinates: [node.longitude, node.latitude] }
  }));
  console.log('points: ', points);

  // 4. use-cluster
  const { clusters, supercluster } = useSupercluster({
    points,
    bounds,
    zoom,
    options: { radius: 75, maxZoom: 20 }
  })
console.log('clusters:', clusters);

How to use this with React Hooks?

How to use this with React Hooks?

e.g. After geoJson is updated, clusters is still an empty.

 const { clusters, supercluster } = useSuperCluster({
    points: geoJson,
    bounds,
    zoom,
    options: { radius: 75, maxZoom: 20 }
  });

useEffect(), 'stores' is dynamically fetched from API.

 useEffect(() => {
    if (stores.length) {
      setCoordinates({ lat: stores[3].latitude, lng: stores[3].longitude });

      const filterStores = stores.filter((item) => {
        return item.latitude && item.longitude;
      });

      const geo: {
        type: string;
        properties: any;
        geometry: { type: string; coordinates: [number, number] };
      }[] = [];

      filterStores.forEach((data) => {
        geo.push({
          type: 'Feature',
          properties: { ...data, cluster: false },
          geometry: { type: 'Point', coordinates: [data.longitude, data.latitude] }
        });
      });

      setGeoJson(geo);
    }
  }, [stores]);

e.g. Data - supercluster has data but clusters do not.

Screen Shot 2022-09-21 at 10 56 30 AM

Dominant point in the cluster that would be displayed on top

It is rather question than issue.

I have huge array of companies. Some companies are really big and would like to display those as the "cluster marker" if one is included in the cluster. Is it possible that use-supercluster hook would return { properties: { cluster: true, hasDominantCompany: true, dominantCompanyName: "Big Company" } }. So it would look like -> at this region of the maps there is this big company + 6 others. Thank you.

Maximum update depth issue caused by using options inline with `map` and `reduce`

Hey there, I was stuck on this for a while but eventually realised I needed to define options as an object outside of my Component (apologies if I missed this somewhere in the docs?).

I guess that the functions are getting reinstantiated somewhere inside the component and causing this infinite loop? Anyway its annoying because the Type safety is decent when used inline which led me to think it would be safe.. will try make a PR to fix :)

[Question] - Leaflet test with 500k points

Hi,

I have a question. Did you try to use this library with 500k points and react-leaflet's markers?

I am asking this question because I came across performance issue, where Leaflet initial rendering takes ~30 seconds with ~40K react-leaflet's GeoJSONs in canvas mode.

I needed to use leaflet's GeoJSON, instead of react-leaflet GeoJSON. I am not sure if this is an issue on my side or library side, but I assume I would have the same issue with react-leaflet's markers.

Multiple markers on the same location

Hello!

I have a couple of markers that are at the same location/address.

To my specified maximum zoom, I still want a cluster to be shown there. However, I can't access those markers any more because they are a cluster. Looking at the properties of the cluster there is the "point_count" which says 2. But I want to have access to the actual markers and any property I have set on them.

Thus, a cluster has no knowledge of the contents of the points used to generate the cluster.

Is there away around this?

Thank you.

Spreading markers with the same geolocation

Hello guys, me again, good morning

Would you have any suggestion on how i could show different markers from same location? i am able to get the children of the cluster, but i would like do spread them very closely where i could click on them,

on leaflet i saw something like this

image

spidering would be great, but just showing different markers for the same locations would be awesome

Reduce option example is incorrect

The reduce function is for merging two clusters which each have their own separate count.

As it is now, the acc.count is counting the number of times a merge has occurred which is not the same as point_count.
To make the example correct, acc.count += 1 should be acc.count += props.count.

Missing index.js file inside dist directory

After installing use-supercluster package, package.json indicates as main: dist/index.js ("main": "dist/index.js",), but there is no index.js file inside dist directory.
image

And i am getting error when running vitest tests,
image

Help with types

This is my situation:

const demoDevices: Array<PointFeature<GeoJsonProperties>> = [
  {
    type: 'Feature',
    properties: { someProps: 13 },
    geometry: {
      type: 'Point',
      coordinates: [-0.1483154296875, 51.54804306453371],
    },
  },
];
{ clusters.map((point, i) => {
  const [longitude, latitude] = point.geometry.coordinates;

  const {
    cluster: isCluster, <- this is type any
    point_count: pointCount, <- this is type any
  } = point.properties; <- all props are type any
}
}

All the properties are type any and that is problem. I can change that by editing types here by putting ClusterProperties instead of GeoJsonProperties and then my cluster, point_count is typed. But downside is that I must specify those props in my geojson array.

const demoDevices: Array<PointFeature<ClusterProperties>> = [{}]

Is there any better way to do what I want than this.
I would define geojson array like this:

const demoDevices: Array<PointFeature<Partial<ClusterProperties> & SomeTypeThatDefinesMyProperties>> = [{}]

This way I have access to all my properties and cluster properties are properly typed. Am I missing something, how would you accomplish this

Can't retrieve leaves from supercluster when too many leaves are present

Hello, I am trying to use the method supercluster.getLeaves(clusterId) to retrieve all of the leaves of a cluster for the case when there are too many points to visualize at the lowest level but it seem not to be working. The method works only for clusters that are able to split apart further but not with the ones that present a very dense points concentration (like shown here https://youtu.be/3HYvbP2pQRA?t=2117 ). Is this a confimed issue?

How to fit bounds when clusters points change?

As the title, there is a way to handle points change out of the current viewport?

Cause inspected the clusters variable, it is filled only when the cluster is visible in the map

I'm using google-map-react

Thank you, Riccardo

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.