('simple')
23 |
24 | const renderExample = () => {
25 | switch (currentExample) {
26 | case 'simple':
27 | return
28 | case 'ten-thousand':
29 | return
30 | case 'custom':
31 | return
32 | default:
33 | return
34 | }
35 | }
36 |
37 | return (
38 |
39 |
React Leaflet Cluster Examples
40 |
41 |
84 |
85 | {renderExample()}
86 |
87 | )
88 | }
89 |
--------------------------------------------------------------------------------
/examples/vite-example/src/components/CustomMarkerCluster.css:
--------------------------------------------------------------------------------
1 | .custom-marker-cluster {
2 | background: #fff;
3 | border: 3px solid #f00800;
4 | border-radius: 50%;
5 | color: #f00800;
6 | height: 33px;
7 | line-height: 27px;
8 | text-align: center;
9 | width: 33px;
10 | font-weight: bold;
11 | font-size: 14px;
12 | }
--------------------------------------------------------------------------------
/examples/vite-example/src/components/CustomMarkerCluster.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react'
2 | import { MapContainer, Marker, TileLayer } from 'react-leaflet'
3 | import L from 'leaflet'
4 | import MarkerClusterGroup from 'react-leaflet-cluster'
5 | import './CustomMarkerCluster.css'
6 |
7 | const customIcon = new L.Icon({
8 | iconUrl:
9 | 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNDAiIGhlaWdodD0iNDciIHZpZXdCb3g9IjAgMCA0MCA0NyIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTIwIDBDOC45NTQzMSAwIDAgOC45NTQzMSAwIDIwQzAgMzEuMDQ1NyA4Ljk1NDMxIDQwIDIwIDQwQzMxLjA0NTcgNDAgNDAgMzEuMDQ1NyA0MCAyMEM0MCA4Ljk1NDMxIDMxLjA0NTcgMCAyMCAwWiIgZmlsbD0iIzAwNzNGQSIvPgo8cGF0aCBkPSJNMjAgNkMxMi4yNjg5IDYgNiAxMi4yNjg5IDYgMjBDNiAyNy43MzExIDEyLjI2ODkgMzQgMjAgMzRDMjcuNzMxMSAzNCAzNCAyNy43MzExIDM0IDIwQzM0IDEyLjI2ODkgMjcuNzMxMSA2IDIwIDZaIiBmaWxsPSJ3aGl0ZSIvPgo8L3N2Zz4K',
10 | iconSize: new L.Point(40, 47),
11 | })
12 |
13 | const createClusterCustomIcon = function (cluster: any) {
14 | return L.divIcon({
15 | html: `${cluster.getChildCount()}`,
16 | className: 'custom-marker-cluster',
17 | iconSize: L.point(33, 33, true),
18 | })
19 | }
20 |
21 | export default function CustomMarkerCluster() {
22 | const [dynamicPosition, setPosition] = useState([41.051687, 28.987261])
23 |
24 | return (
25 |
26 |
Custom Marker Cluster Example
27 |
34 |
40 |
44 | console.log('onClick', e)}
46 | iconCreateFunction={createClusterCustomIcon as any}
47 | maxClusterRadius={150}
48 | spiderfyOnMaxZoom={true}
49 | polygonOptions={{
50 | fillColor: '#ffffff',
51 | color: '#f00800',
52 | weight: 5,
53 | opacity: 1,
54 | fillOpacity: 0.8,
55 | }}
56 | showCoverageOnHover={true}
57 | >
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 | )
68 | }
69 |
--------------------------------------------------------------------------------
/examples/vite-example/src/components/SimpleExample.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react'
2 | import { MapContainer, Marker, TileLayer } from 'react-leaflet'
3 | import MarkerClusterGroup from 'react-leaflet-cluster'
4 |
5 | export default function SimpleExample() {
6 | const [count, setCount] = useState(0)
7 |
8 | return (
9 |
10 |
Simple Example
11 |
12 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 | )
28 | }
29 |
--------------------------------------------------------------------------------
/examples/vite-example/src/components/TenThousandMarker.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { MapContainer, Marker, TileLayer } from 'react-leaflet'
3 | import MarkerClusterGroup from 'react-leaflet-cluster'
4 |
5 | // Mock data for 10,000 markers
6 | const generateAddressPoints = () => {
7 | const points = []
8 | for (let i = 0; i < 10000; i++) {
9 | points.push([
10 | -41.975762 + (Math.random() - 0.5) * 20, // latitude
11 | 172.934298 + (Math.random() - 0.5) * 20, // longitude
12 | `Marker ${i + 1}`, // title
13 | ])
14 | }
15 | return points
16 | }
17 |
18 | const addressPoints = generateAddressPoints()
19 | type AddressPoint = Array<[number, number, string]>
20 |
21 | export default function TenThousandMarker() {
22 | return (
23 |
24 |
10,000 Markers Example
25 |
31 |
35 |
36 | {(addressPoints as AddressPoint).map((address, index) => (
37 |
38 | ))}
39 |
40 |
41 |
42 | )
43 | }
44 |
--------------------------------------------------------------------------------
/examples/vite-example/src/main.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import ReactDOM from 'react-dom/client'
3 | import App from './App.tsx'
4 |
5 | ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
6 |
7 |
8 | ,
9 | )
10 |
--------------------------------------------------------------------------------
/examples/vite-example/src/vite-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/examples/vite-example/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ES2020",
4 | "useDefineForClassFields": true,
5 | "lib": ["ES2020", "DOM", "DOM.Iterable"],
6 | "module": "ESNext",
7 | "skipLibCheck": true,
8 |
9 | /* Bundler mode */
10 | "moduleResolution": "bundler",
11 | "allowImportingTsExtensions": true,
12 | "resolveJsonModule": true,
13 | "isolatedModules": true,
14 | "noEmit": true,
15 | "jsx": "react-jsx",
16 |
17 | /* Linting */
18 | "strict": true,
19 | "noUnusedLocals": true,
20 | "noUnusedParameters": true,
21 | "noFallthroughCasesInSwitch": true
22 | },
23 | "include": ["src"],
24 | "references": [{ "path": "./tsconfig.node.json" }]
25 | }
26 |
--------------------------------------------------------------------------------
/examples/vite-example/tsconfig.node.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "composite": true,
4 | "skipLibCheck": true,
5 | "module": "ESNext",
6 | "moduleResolution": "bundler",
7 | "allowSyntheticDefaultImports": true
8 | },
9 | "include": ["vite.config.ts"]
10 | }
11 |
--------------------------------------------------------------------------------
/examples/vite-example/vite.config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'vite'
2 | import react from '@vitejs/plugin-react'
3 |
4 | // https://vitejs.dev/config/
5 | export default defineConfig({
6 | plugins: [react()],
7 | })
8 |
--------------------------------------------------------------------------------
/examples/vite-example/vite.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
10 |
11 |
12 |
13 |
17 |
18 | birikimplani 🚀🚀🚀
19 |
20 |
21 |
22 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-leaflet-cluster",
3 | "version": "3.1.1",
4 | "description": "React-leaflet-cluster is a plugin for react-leaflet. A wrapper component of Leaflet.markercluster.",
5 | "main": "dist/index.js",
6 | "type": "module",
7 | "types": "dist/index.d.ts",
8 | "files": [
9 | "dist"
10 | ],
11 | "scripts": {
12 | "build": "tsc && npm run copy:assets",
13 | "format": "prettier --write \"src/**/*.tsx\"",
14 | "lint": "tslint -p tsconfig.json",
15 | "prepublishOnly": "npm run lint",
16 | "preversion": "npm run lint",
17 | "version": "npm run format && git add -A src",
18 | "postversion": "git push && git push --tags",
19 | "copy:assets": "cpx 'src/assets/**' 'dist/assets'"
20 | },
21 | "dependencies": {
22 | "leaflet.markercluster": "^1.5.3"
23 | },
24 | "peerDependencies": {
25 | "leaflet": "^1.8.0",
26 | "react": "^18.2.0 || ^19.0.0",
27 | "react-dom": "^18.2.0 || ^19.0.0",
28 | "react-leaflet": "^4.0.0"
29 | },
30 | "devDependencies": {
31 | "@types/leaflet": "^1.7.11",
32 | "@types/leaflet.markercluster": "^1.5.0",
33 | "@types/node": "^14.18.21",
34 | "@types/react": "^18.2.0",
35 | "@types/react-dom": "^18.2.0",
36 | "@typescript-eslint/eslint-plugin": "^4.33.0",
37 | "@typescript-eslint/parser": "^4.33.0",
38 | "cpx": "^1.5.0",
39 | "eslint": "^7.32.0",
40 | "eslint-loader": "^4.0.2",
41 | "eslint-plugin-prettier": "^3.4.1",
42 | "eslint-plugin-react": "^7.30.0",
43 | "eslint-plugin-react-hooks": "^4.5.0",
44 | "leaflet": "^1.8.0",
45 | "prettier": "^2.6.2",
46 | "react": "^18.0.0",
47 | "react-dom": "^18.0.0",
48 | "react-leaflet": "^4.0.0",
49 | "ts-loader": "^8.4.0",
50 | "tslint": "^6.1.3",
51 | "tslint-config-prettier": "^1.18.0",
52 | "typescript": "^4.7.3",
53 | "uglify-js": "^3.16.0"
54 | },
55 | "author": "akursat",
56 | "homepage": "https://akursat.gitbook.io/marker-cluster/",
57 | "license": "SEE LICENSE IN ",
58 | "repository": "https://github.com/akursat/react-leaflet-cluster",
59 | "keywords": [
60 | "react",
61 | "leaflet",
62 | "marker-cluster",
63 | "cluster",
64 | "map",
65 | "react-leaflet-v4"
66 | ]
67 | }
68 |
--------------------------------------------------------------------------------
/showcase.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/akursat/react-leaflet-cluster/c4775915d8b8667bb91612155ee0309cc7b2a7ed/showcase.gif
--------------------------------------------------------------------------------
/src/assets/MarkerCluster.Default.css:
--------------------------------------------------------------------------------
1 | /* To solve Next.js issues source from https://github.com/Leaflet/Leaflet.markercluster/blob/master/dist/MarkerCluster.Default.css */
2 | .marker-cluster-small {
3 | background-color: rgba(181, 226, 140, 0.6);
4 | }
5 | .marker-cluster-small div {
6 | background-color: rgba(110, 204, 57, 0.6);
7 | }
8 |
9 | .marker-cluster-medium {
10 | background-color: rgba(241, 211, 87, 0.6);
11 | }
12 | .marker-cluster-medium div {
13 | background-color: rgba(240, 194, 12, 0.6);
14 | }
15 |
16 | .marker-cluster-large {
17 | background-color: rgba(253, 156, 115, 0.6);
18 | }
19 | .marker-cluster-large div {
20 | background-color: rgba(241, 128, 23, 0.6);
21 | }
22 |
23 | /* IE 6-8 fallback colors */
24 | .leaflet-oldie .marker-cluster-small {
25 | background-color: rgb(181, 226, 140);
26 | }
27 | .leaflet-oldie .marker-cluster-small div {
28 | background-color: rgb(110, 204, 57);
29 | }
30 |
31 | .leaflet-oldie .marker-cluster-medium {
32 | background-color: rgb(241, 211, 87);
33 | }
34 | .leaflet-oldie .marker-cluster-medium div {
35 | background-color: rgb(240, 194, 12);
36 | }
37 |
38 | .leaflet-oldie .marker-cluster-large {
39 | background-color: rgb(253, 156, 115);
40 | }
41 | .leaflet-oldie .marker-cluster-large div {
42 | background-color: rgb(241, 128, 23);
43 | }
44 |
45 | .marker-cluster {
46 | background-clip: padding-box;
47 | border-radius: 20px;
48 | }
49 | .marker-cluster div {
50 | width: 30px;
51 | height: 30px;
52 | margin-left: 5px;
53 | margin-top: 5px;
54 |
55 | text-align: center;
56 | border-radius: 15px;
57 | font: 12px "Helvetica Neue", Arial, Helvetica, sans-serif;
58 | }
59 | .marker-cluster span {
60 | line-height: 30px;
61 | }
--------------------------------------------------------------------------------
/src/assets/MarkerCluster.css:
--------------------------------------------------------------------------------
1 | /* To solve Next.js issues source from https://github.com/Leaflet/Leaflet.markercluster/blob/master/dist/MarkerCluster.css */
2 | .leaflet-cluster-anim .leaflet-marker-icon, .leaflet-cluster-anim .leaflet-marker-shadow {
3 | -webkit-transition: -webkit-transform 0.3s ease-out, opacity 0.3s ease-in;
4 | -moz-transition: -moz-transform 0.3s ease-out, opacity 0.3s ease-in;
5 | -o-transition: -o-transform 0.3s ease-out, opacity 0.3s ease-in;
6 | transition: transform 0.3s ease-out, opacity 0.3s ease-in;
7 | }
8 |
9 | .leaflet-cluster-spider-leg {
10 | /* stroke-dashoffset (duration and function) should match with leaflet-marker-icon transform in order to track it exactly */
11 | -webkit-transition: -webkit-stroke-dashoffset 0.3s ease-out, -webkit-stroke-opacity 0.3s ease-in;
12 | -moz-transition: -moz-stroke-dashoffset 0.3s ease-out, -moz-stroke-opacity 0.3s ease-in;
13 | -o-transition: -o-stroke-dashoffset 0.3s ease-out, -o-stroke-opacity 0.3s ease-in;
14 | transition: stroke-dashoffset 0.3s ease-out, stroke-opacity 0.3s ease-in;
15 | }
--------------------------------------------------------------------------------
/src/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import {
3 | extendContext,
4 | createElementObject,
5 | createPathComponent,
6 | LeafletContextInterface,
7 | } from '@react-leaflet/core'
8 | import L, { LeafletMouseEventHandlerFn } from 'leaflet'
9 | import 'leaflet.markercluster'
10 | // CSS imports removed to prevent Next.js issues
11 | // Users should import CSS separately:
12 | // import 'react-leaflet-cluster/dist/assets/MarkerCluster.css'
13 | // import 'react-leaflet-cluster/dist/assets/MarkerCluster.Default.css'
14 |
15 | // Users should configure their own icon URLs as needed
16 | // delete (L.Icon.Default as any).prototype._getIconUrl
17 | // L.Icon.Default.mergeOptions({
18 | // iconRetinaUrl: new URL('./assets/marker-icon-2x.png', import.meta.url).href,
19 | // iconUrl: new URL('./assets/marker-icon.png', import.meta.url).href,
20 | // shadowUrl: new URL('./assets/marker-shadow.png', import.meta.url).href,
21 | // })
22 |
23 | type ClusterType = { [key in string]: any }
24 |
25 | type ClusterEvents = {
26 | onClick?: LeafletMouseEventHandlerFn
27 | onDblClick?: LeafletMouseEventHandlerFn
28 | onMouseDown?: LeafletMouseEventHandlerFn
29 | onMouseUp?: LeafletMouseEventHandlerFn
30 | onMouseOver?: LeafletMouseEventHandlerFn
31 | onMouseOut?: LeafletMouseEventHandlerFn
32 | onContextMenu?: LeafletMouseEventHandlerFn
33 | }
34 |
35 | type MarkerClusterControl = L.MarkerClusterGroupOptions & {
36 | children: React.ReactNode
37 | } & ClusterEvents
38 |
39 | function getPropsAndEvents(props: MarkerClusterControl) {
40 | let clusterProps: ClusterType = {}
41 | let clusterEvents: ClusterType = {}
42 | const { children, ...rest } = props
43 | // Splitting props and events to different objects
44 | Object.entries(rest).forEach(([propName, prop]) => {
45 | if (propName.startsWith('on')) {
46 | clusterEvents = { ...clusterEvents, [propName]: prop }
47 | } else {
48 | clusterProps = { ...clusterProps, [propName]: prop }
49 | }
50 | })
51 | return { clusterProps, clusterEvents }
52 | }
53 |
54 | function createMarkerClusterGroup(props: MarkerClusterControl, context: LeafletContextInterface) {
55 | const { clusterProps, clusterEvents } = getPropsAndEvents(props)
56 | const markerClusterGroup = new L.MarkerClusterGroup(clusterProps)
57 | Object.entries(clusterEvents).forEach(([eventAsProp, callback]) => {
58 | const clusterEvent = `cluster${eventAsProp.substring(2).toLowerCase()}`
59 | markerClusterGroup.on(clusterEvent, callback)
60 | })
61 | return createElementObject(
62 | markerClusterGroup,
63 | extendContext(context, { layerContainer: markerClusterGroup }),
64 | )
65 | }
66 |
67 | const updateMarkerCluster = (
68 | instance: L.MarkerClusterGroup,
69 | props: MarkerClusterControl,
70 | prevProps: MarkerClusterControl,
71 | ) => {
72 | //TODO when prop change update instance
73 | // if (props. !== prevProps.center || props.size !== prevProps.size) {
74 | // instance.setBounds(getBounds(props))
75 | // }
76 | }
77 |
78 | const MarkerClusterGroup = createPathComponent(
79 | createMarkerClusterGroup,
80 | updateMarkerCluster,
81 | )
82 |
83 | export default MarkerClusterGroup
84 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ES2020",
4 | "module": "ES2020",
5 | "moduleResolution": "node",
6 | "declaration": true,
7 | "outDir": "./dist",
8 | "strict": true,
9 | "jsx": "react-jsx",
10 | "esModuleInterop": true,
11 | "allowSyntheticDefaultImports": true,
12 | "skipLibCheck": true
13 | },
14 | "include": ["src"],
15 | "exclude": ["node_modules", "**/__tests__/*"]
16 | }
--------------------------------------------------------------------------------