├── .DS_Store ├── examples ├── vite-example │ ├── src │ │ ├── vite-env.d.ts │ │ ├── main.tsx │ │ ├── components │ │ │ ├── CustomMarkerCluster.css │ │ │ ├── SimpleExample.tsx │ │ │ ├── TenThousandMarker.tsx │ │ │ └── CustomMarkerCluster.tsx │ │ └── App.tsx │ ├── tsconfig.node.json │ ├── vite.config.ts │ ├── .gitignore │ ├── index.html │ ├── .eslintrc.cjs │ ├── tsconfig.json │ ├── package.json │ ├── public │ │ ├── MarkerCluster.css │ │ ├── vite.svg │ │ └── MarkerCluster.Default.css │ └── vite.svg └── .DS_Store ├── showcase.gif ├── .prettierrc.yml ├── CONTRIBUTING.md ├── .gitignore ├── tsconfig.json ├── .eslintrc.js ├── dist ├── index.d.ts ├── assets │ ├── MarkerCluster.css │ └── MarkerCluster.Default.css └── index.js ├── index.html ├── src ├── assets │ ├── MarkerCluster.css │ └── MarkerCluster.Default.css └── index.tsx ├── LICENSE ├── package.json └── README.md /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/akursat/react-leaflet-cluster/HEAD/.DS_Store -------------------------------------------------------------------------------- /examples/vite-example/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /showcase.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/akursat/react-leaflet-cluster/HEAD/showcase.gif -------------------------------------------------------------------------------- /examples/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/akursat/react-leaflet-cluster/HEAD/examples/.DS_Store -------------------------------------------------------------------------------- /.prettierrc.yml: -------------------------------------------------------------------------------- 1 | semi: false 2 | trailingComma: 'all' 3 | singleQuote: true 4 | printWidth: 100 5 | tabWidth: 2 -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ## Prerequisites 2 | Build package locally and test it with example project with `npm link` command. -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /coverage 2 | /demo/dist 3 | /es 4 | /lib 5 | /node_modules 6 | /umd 7 | /examples/custom-marker-cluster/.yalc 8 | /examples/ten-thousand-marker/.yalc 9 | .yalc 10 | npm-debug.log* 11 | -------------------------------------------------------------------------------- /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/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/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/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 | resolve: { 8 | // Deduplicate dependencies to avoid multiple React instances when using npm link 9 | dedupe: ['@react-leaflet/core'], 10 | }, 11 | }) 12 | -------------------------------------------------------------------------------- /examples/vite-example/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /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 | } -------------------------------------------------------------------------------- /examples/vite-example/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Vite + React + TS + Leaflet + Marker Cluster 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /examples/vite-example/.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { browser: true, es2020: true }, 3 | extends: [ 4 | 'eslint:recommended', 5 | 'plugin:@typescript-eslint/recommended', 6 | 'plugin:react-hooks/recommended', 7 | ], 8 | parser: '@typescript-eslint/parser', 9 | parserOptions: { ecmaVersion: 'latest', sourceType: 'module' }, 10 | plugins: ['react-refresh'], 11 | rules: { 12 | 'react-refresh/only-export-components': 'warn', 13 | }, 14 | } 15 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: '@typescript-eslint/parser', 3 | parserOptions: { 4 | ecmaVersion: 2020, 5 | sourceType: 'module', 6 | ecmaFeatures: { 7 | jsx: true, 8 | }, 9 | }, 10 | settings: { 11 | react: { 12 | version: 'detect', 13 | }, 14 | }, 15 | extends: [ 16 | 'plugin:react/recommended', 17 | 'plugin:@typescript-eslint/recommended', 18 | 'plugin:prettier/recommended', 19 | ], 20 | plugins: ['react-hooks'], 21 | rules: { 22 | 'react-hooks/rules-of-hooks': 'error', 23 | 'react-hooks/exhaustive-deps': 'warn', 24 | }, 25 | }; -------------------------------------------------------------------------------- /dist/index.d.ts: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import L, { LeafletMouseEventHandlerFn } from 'leaflet'; 3 | 4 | type ClusterEvents = { 5 | onClick?: LeafletMouseEventHandlerFn; 6 | onDblClick?: LeafletMouseEventHandlerFn; 7 | onMouseDown?: LeafletMouseEventHandlerFn; 8 | onMouseUp?: LeafletMouseEventHandlerFn; 9 | onMouseOver?: LeafletMouseEventHandlerFn; 10 | onMouseOut?: LeafletMouseEventHandlerFn; 11 | onContextMenu?: LeafletMouseEventHandlerFn; 12 | }; 13 | declare const MarkerClusterGroup: React.ForwardRefExoticComponent>; 16 | 17 | export { MarkerClusterGroup as default }; 18 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 10 | 11 | 12 | 13 | 17 | 18 | birikimplani 🚀🚀🚀 19 | 20 | 21 |
22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /examples/vite-example/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "my-vite-app", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "tsc && vite build", 9 | "lint": "eslint src --ext ts,tsx --report-unused-disable-directives --max-warnings 0", 10 | "preview": "vite preview" 11 | }, 12 | "dependencies": { 13 | "leaflet": "^1.9.0", 14 | "react": "^19.0.0", 15 | "react-dom": "^19.0.0", 16 | "react-leaflet": "^5.0.0", 17 | "react-leaflet-cluster": "^4.0.0" 18 | }, 19 | "devDependencies": { 20 | "@types/leaflet": "^1.9.3", 21 | "@types/react": "^19.0.0", 22 | "@types/react-dom": "^19.0.0", 23 | "@typescript-eslint/eslint-plugin": "^5.59.0", 24 | "@typescript-eslint/parser": "^5.59.0", 25 | "@vitejs/plugin-react": "^4.0.0", 26 | "eslint": "^8.38.0", 27 | "eslint-plugin-react-hooks": "^4.6.0", 28 | "eslint-plugin-react-refresh": "^0.3.4", 29 | "typescript": "^5.0.2", 30 | "vite": "^4.3.9" 31 | } 32 | } -------------------------------------------------------------------------------- /dist/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/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 | } -------------------------------------------------------------------------------- /examples/vite-example/public/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 | } -------------------------------------------------------------------------------- /examples/vite-example/src/components/SimpleExample.tsx: -------------------------------------------------------------------------------- 1 | import { 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 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 A. Kürşat Uzun 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /examples/vite-example/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/vite-example/public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/vite-example/src/components/TenThousandMarker.tsx: -------------------------------------------------------------------------------- 1 | import { MapContainer, Marker, TileLayer } from 'react-leaflet' 2 | import MarkerClusterGroup from 'react-leaflet-cluster' 3 | 4 | // Mock data for 10,000 markers 5 | const generateAddressPoints = () => { 6 | const points = [] 7 | for (let i = 0; i < 10000; i++) { 8 | points.push([ 9 | -41.975762 + (Math.random() - 0.5) * 20, // latitude 10 | 172.934298 + (Math.random() - 0.5) * 20, // longitude 11 | `Marker ${i + 1}`, // title 12 | ]) 13 | } 14 | return points 15 | } 16 | 17 | const addressPoints = generateAddressPoints() 18 | type AddressPoint = Array<[number, number, string]> 19 | 20 | export default function TenThousandMarker() { 21 | return ( 22 |
23 |

10,000 Markers Example

24 | 30 | 34 | 35 | {(addressPoints as AddressPoint).map((address, index) => ( 36 | 37 | ))} 38 | 39 | 40 |
41 | ) 42 | } 43 | -------------------------------------------------------------------------------- /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 | } -------------------------------------------------------------------------------- /dist/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 | } -------------------------------------------------------------------------------- /examples/vite-example/public/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 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-leaflet-cluster", 3 | "version": "4.0.0", 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": "tsup src/index.tsx --format esm --dts --clean --external @react-leaflet/core && npm run copy:assets", 13 | "format": "prettier --write \"src/**/*.tsx\"", 14 | "lint": "eslint 'src/**/*.{ts,tsx}'", 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 | "@react-leaflet/core": "^3.0.0", 26 | "leaflet": "^1.9.0", 27 | "react": "^19.0.0", 28 | "react-dom": "^19.0.0", 29 | "react-leaflet": "^5.0.0" 30 | }, 31 | "devDependencies": { 32 | "@types/leaflet": "^1.9.0", 33 | "@types/leaflet.markercluster": "^1.5.4", 34 | "@types/node": "^14.18.21", 35 | "@types/react": "^19.0.0", 36 | "@types/react-dom": "^19.0.0", 37 | "@typescript-eslint/eslint-plugin": "^4.33.0", 38 | "@typescript-eslint/parser": "^4.33.0", 39 | "cpx": "^1.2.1", 40 | "eslint": "^7.32.0", 41 | "eslint-config-prettier": "^10.1.8", 42 | "eslint-plugin-prettier": "^3.4.1", 43 | "eslint-plugin-react": "^7.30.0", 44 | "eslint-plugin-react-hooks": "^4.5.0", 45 | "prettier": "^2.6.2", 46 | "react-leaflet": "^5.0.0", 47 | "tsup": "^8.0.0", 48 | "typescript": "^5.9.3" 49 | }, 50 | "author": "akursat", 51 | "homepage": "https://akursat.gitbook.io/marker-cluster/", 52 | "license": "SEE LICENSE IN ", 53 | "repository": "https://github.com/akursat/react-leaflet-cluster", 54 | "keywords": [ 55 | "react", 56 | "leaflet", 57 | "marker-cluster", 58 | "cluster", 59 | "map", 60 | "react-leaflet-v4" 61 | ] 62 | } -------------------------------------------------------------------------------- /dist/index.js: -------------------------------------------------------------------------------- 1 | // src/index.tsx 2 | import { 3 | extendContext, 4 | createElementObject, 5 | createPathComponent 6 | } from "@react-leaflet/core"; 7 | import L from "leaflet"; 8 | import "leaflet.markercluster"; 9 | function getPropsAndEvents(props) { 10 | let clusterProps = {}; 11 | let clusterEvents = {}; 12 | const { children, ...rest } = props; 13 | Object.entries(rest).forEach(([propName, prop]) => { 14 | if (propName.startsWith("on")) { 15 | clusterEvents = { ...clusterEvents, [propName]: prop }; 16 | } else { 17 | clusterProps = { ...clusterProps, [propName]: prop }; 18 | } 19 | }); 20 | return { clusterProps, clusterEvents }; 21 | } 22 | function createMarkerClusterGroup(props, context) { 23 | const { clusterProps, clusterEvents } = getPropsAndEvents(props); 24 | const markerClusterGroup = new L.MarkerClusterGroup(clusterProps); 25 | Object.entries(clusterEvents).forEach(([eventAsProp, callback]) => { 26 | const clusterEvent = `cluster${eventAsProp.substring(2).toLowerCase()}`; 27 | markerClusterGroup.on(clusterEvent, callback); 28 | }); 29 | return createElementObject( 30 | markerClusterGroup, 31 | extendContext(context, { layerContainer: markerClusterGroup }) 32 | ); 33 | } 34 | var updateMarkerCluster = (instance, props, prevProps) => { 35 | const { clusterProps, clusterEvents } = getPropsAndEvents(props); 36 | const { clusterProps: prevClusterProps, clusterEvents: prevClusterEvents } = getPropsAndEvents(prevProps); 37 | Object.keys(clusterProps).forEach((key) => { 38 | if (clusterProps[key] !== prevClusterProps[key]) { 39 | instance.options[key] = clusterProps[key]; 40 | } 41 | }); 42 | Object.entries(prevClusterEvents).forEach(([eventAsProp, callback]) => { 43 | const clusterEvent = `cluster${eventAsProp.substring(2).toLowerCase()}`; 44 | instance.off(clusterEvent, callback); 45 | }); 46 | Object.entries(clusterEvents).forEach(([eventAsProp, callback]) => { 47 | const clusterEvent = `cluster${eventAsProp.substring(2).toLowerCase()}`; 48 | instance.on(clusterEvent, callback); 49 | }); 50 | }; 51 | var MarkerClusterGroup = createPathComponent( 52 | createMarkerClusterGroup, 53 | updateMarkerCluster 54 | ); 55 | var index_default = MarkerClusterGroup; 56 | export { 57 | index_default as default 58 | }; 59 | -------------------------------------------------------------------------------- /examples/vite-example/src/components/CustomMarkerCluster.tsx: -------------------------------------------------------------------------------- 1 | import { 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/App.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react' 2 | import 'leaflet/dist/leaflet.css' 3 | // Import the required CSS for marker clustering 4 | import 'react-leaflet-cluster/dist/assets/MarkerCluster.css' 5 | import 'react-leaflet-cluster/dist/assets/MarkerCluster.Default.css' 6 | 7 | import SimpleExample from './components/SimpleExample' 8 | import TenThousandMarker from './components/TenThousandMarker' 9 | import CustomMarkerCluster from './components/CustomMarkerCluster' 10 | import L from 'leaflet' 11 | 12 | // configure the default icon 13 | delete (L.Icon.Default as any).prototype._getIconUrl 14 | L.Icon.Default.mergeOptions({ 15 | iconRetinaUrl: 'https://unpkg.com/leaflet@1.9.4/dist/images/marker-icon-2x.png', 16 | iconUrl: 'https://unpkg.com/leaflet@1.9.4/dist/images/marker-icon.png', 17 | shadowUrl: 'https://unpkg.com/leaflet@1.9.4/dist/images/marker-shadow.png', 18 | }) 19 | type ExampleType = 'simple' | 'ten-thousand' | 'custom' 20 | 21 | export default function App() { 22 | const [currentExample, setCurrentExample] = useState('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 | -------------------------------------------------------------------------------- /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 | const { clusterProps, clusterEvents } = getPropsAndEvents(props) 73 | const { clusterProps: prevClusterProps, clusterEvents: prevClusterEvents } = 74 | getPropsAndEvents(prevProps) 75 | 76 | // Update Options 77 | Object.keys(clusterProps).forEach((key) => { 78 | if (clusterProps[key] !== prevClusterProps[key]) { 79 | // eslint-disable-next-line 80 | // @ts-ignore 81 | instance.options[key] = clusterProps[key] 82 | } 83 | }) 84 | 85 | // Update Events 86 | Object.entries(prevClusterEvents).forEach(([eventAsProp, callback]) => { 87 | const clusterEvent = `cluster${eventAsProp.substring(2).toLowerCase()}` 88 | instance.off(clusterEvent, callback) 89 | }) 90 | 91 | Object.entries(clusterEvents).forEach(([eventAsProp, callback]) => { 92 | const clusterEvent = `cluster${eventAsProp.substring(2).toLowerCase()}` 93 | instance.on(clusterEvent, callback) 94 | }) 95 | } 96 | 97 | const MarkerClusterGroup = createPathComponent( 98 | createMarkerClusterGroup, 99 | updateMarkerCluster, 100 | ) 101 | 102 | export default MarkerClusterGroup 103 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # react-leaflet-cluster [![NPM version](https://badgen.net/npm/v/react-leaflet-cluster)](https://npmjs.com/package/react-leaflet-cluster) [![NPM downloads](https://badgen.net/npm/dm/react-leaflet-cluster)](https://npmjs.com/package/react-leaflet-cluster) 2 | 3 | - [x] React 19 support 4 | - [x] React-leaflet v5 support 5 | - [x] Typescript support 6 | - [x] Next.js compatibility 7 | 8 | ## Breaking Changes in v4.0.0 9 | This release updates key peer dependencies to support React 19 and React-Leaflet 5. Make sure your project is upgraded before installing this version. 10 | 11 | ## Breaking Changes in v3.0.0 12 | 13 | **CSS imports are now required manually** - The package no longer automatically imports CSS files to prevent Next.js build issues. You must now import the CSS files separately: 14 | 15 | ```tsx 16 | import 'react-leaflet-cluster/dist/assets/MarkerCluster.css' 17 | import 'react-leaflet-cluster/dist/assets/MarkerCluster.Default.css' 18 | ``` 19 | 20 | React-leaflet-cluster is a plugin for react-leaflet. A wrapper component of Leaflet.markercluster. Ready to be integrated into your React.js application to create beautifully animated Marker Clustering functionality. 21 | 22 | ![](showcase.gif) 23 | 24 | ### Examples - Code Sandbox 25 | 26 | - [10.000 marker](https://codesandbox.io/s/hidden-breeze-nrd3e?fontsize=14&hidenavigation=1&theme=dark) 27 | - [Custom marker cluster](https://codesandbox.io/s/beautiful-pike-j2l0w?file=/src/App.tsx) 28 | 29 | ### Installation 30 | 31 | `yarn add react-leaflet-cluster` 32 | 33 | Or with npm: 34 | `npm i react-leaflet-cluster` 35 | 36 | #### Prerequisites 37 | 38 | Make sure that you've installed react-leaflet and leaflet. 39 | 40 | ```json 41 | "react": "19.x", 42 | "react-dom": "19.0.0", 43 | "leaflet": "1.9.x", 44 | "react-leaflet": "5.0.x" 45 | ``` 46 | 47 | #### CSS Import 48 | 49 | The package requires CSS files to be imported for proper styling. Add these imports to your main component or entry file: 50 | 51 | ```tsx 52 | import 'react-leaflet-cluster/dist/assets/MarkerCluster.css' 53 | import 'react-leaflet-cluster/dist/assets/MarkerCluster.Default.css' 54 | ``` 55 | 56 | **Note for Next.js users**: These CSS imports are required and should be added to your component or a global CSS file. The package no longer automatically imports CSS to prevent Next.js build issues. 57 | 58 | #### Icon Configuration 59 | 60 | The package no longer automatically configures Leaflet's default marker icons. If you need to use default markers, you'll need to configure the icon URLs yourself. Add this configuration to your component or entry file: 61 | 62 | ```tsx 63 | import L from 'leaflet' 64 | 65 | // Configure default marker icons 66 | delete (L.Icon.Default as any).prototype._getIconUrl 67 | L.Icon.Default.mergeOptions({ 68 | iconRetinaUrl: 'https://unpkg.com/leaflet@1.9.4/dist/images/marker-icon-2x.png', 69 | iconUrl: 'https://unpkg.com/leaflet@1.9.4/dist/images/marker-icon.png', 70 | shadowUrl: 'https://unpkg.com/leaflet@1.9.4/dist/images/marker-shadow.png', 71 | }) 72 | ``` 73 | 74 | Alternatively, you can use your own custom icons for markers: 75 | 76 | ```tsx 77 | import L from 'leaflet' 78 | 79 | const customIcon = new L.Icon({ 80 | iconUrl: '/path/to/your/marker-icon.png', 81 | iconSize: [25, 41], 82 | iconAnchor: [12, 41], 83 | popupAnchor: [1, -34], 84 | shadowUrl: '/path/to/your/marker-shadow.png', 85 | shadowSize: [41, 41], 86 | }) 87 | ``` 88 | 89 | #### Migration from v2.x 90 | 91 | If you're upgrading from v2.x, you need to add the CSS imports manually. The package will work without them, but the clustering won't be styled properly. 92 | 93 | **Before (v2.x):** 94 | 95 | ```tsx 96 | import MarkerClusterGroup from 'react-leaflet-cluster' 97 | // CSS was automatically imported 98 | ``` 99 | 100 | **After (v3.1.0):** 101 | 102 | ```tsx 103 | import MarkerClusterGroup from 'react-leaflet-cluster' 104 | import 'react-leaflet-cluster/dist/assets/MarkerCluster.css' 105 | import 'react-leaflet-cluster/dist/assets/MarkerCluster.Default.css' 106 | ``` 107 | 108 | #### API 109 | 110 | For more detailed guide and API see: 111 | https://akursat.gitbook.io/marker-cluster/api 112 | 113 | #### Usage 114 | 115 | ```tsx 116 | import MarkerClusterGroup from 'react-leaflet-cluster' 117 | import { MapContainer, Marker } from 'react-leaflet' 118 | import 'leaflet/dist/leaflet.css' 119 | // Import the required CSS for marker clustering 120 | import 'react-leaflet-cluster/dist/assets/MarkerCluster.css' 121 | import 'react-leaflet-cluster/dist/assets/MarkerCluster.Default.css' 122 | import { addressPoints } from './realworld' 123 | 124 | const Demo = () => { 125 | return ( 126 | 132 | 133 | {(addressPoints as AdressPoint).map((address, index) => ( 134 | 140 | ))} 141 | 142 | 143 | ) 144 | } 145 | ``` 146 | --------------------------------------------------------------------------------